TestUtil.java

1
package com.reallifedeveloper.tools.test;
2
3
import java.io.BufferedWriter;
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.lang.reflect.Field;
7
import java.net.ServerSocket;
8
import java.nio.charset.Charset;
9
import java.nio.charset.StandardCharsets;
10
import java.nio.file.Files;
11
import java.nio.file.Paths;
12
import java.text.DateFormat;
13
import java.text.ParseException;
14
import java.text.SimpleDateFormat;
15
import java.time.ZoneOffset;
16
import java.time.ZonedDateTime;
17
import java.util.Arrays;
18
import java.util.Date;
19
import java.util.List;
20
import java.util.Scanner;
21
22
import org.checkerframework.checker.nullness.qual.Nullable;
23
24
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25
26
/**
27
 * Miscellaneous utility methods that are useful when testing.
28
 *
29
 * @author RealLifeDeveloper
30
 */
31
@SuppressWarnings("PMD.TooManyMethods")
32
public final class TestUtil {
33
34
    /**
35
     * The date format used by {@link #parseDate(String)} ({@value #DATE_FORMAT}).
36
     */
37
    public static final String DATE_FORMAT = "yyyy-MM-dd";
38
39
    /**
40
     * The date+time format used by {@link #parseDateTime(String)} ({@value #DATE_TIME_FORMAT}).
41
     */
42
    public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
43
44
    /**
45
     * This is a utility class with only static methods, so we hide the only constructor.
46
     */
47
    private TestUtil() {
48
    }
49
50
    /**
51
     * Gives a port number on the local machine that no server process is listening to.
52
     *
53
     * @return a free port number
54
     *
55
     * @throws IOException if an I/O error occurs when trying to open a socket
56
     */
57
    @SuppressFBWarnings(value = "UNENCRYPTED_SERVER_SOCKET", justification = "Server socket only created temporarily to find free port")
58
    public static int findFreePort() throws IOException {
59
        try (ServerSocket server = new ServerSocket(0)) {
60 1 1. findFreePort : replaced int return with 0 for com/reallifedeveloper/tools/test/TestUtil::findFreePort → KILLED
            return server.getLocalPort();
61
        }
62
    }
63
64
    /**
65
     * Parses a date string on the form {@value #DATE_FORMAT} and returns the corresponding {@code java.util.Date} object.
66
     *
67
     * @param date the date string to parse, should be on the form {@value #DATE_FORMAT}
68
     *
69
     * @return the {@code java.util.Date} corresponding to {@code date}
70
     *
71
     * @throws IllegalArgumentException if {@code date} cannot be parsed
72
     */
73
    public static Date parseDate(String date) {
74 1 1. parseDate : negated conditional → KILLED
        if (date == null) {
75
            throw new IllegalArgumentException("date must not be null");
76
        }
77
        DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
78
        try {
79 1 1. parseDate : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::parseDate → KILLED
            return dateFormat.parse(date);
80
        } catch (ParseException e) {
81
            throw new IllegalArgumentException("Unparseable date: " + date, e);
82
        }
83
    }
84
85
    /**
86
     * Parses a date and time string on the form {@value #DATE_TIME_FORMAT} and returns the corresonding {@code java.util.Date} object.
87
     *
88
     * @param dateTime the date+time string to parse, should be on the form {@value #DATE_TIME_FORMAT}
89
     *
90
     * @return the {@code java.util.Date} corresponding to {@code dateTime}
91
     *
92
     * @throws IllegalArgumentException if {@code dateTime} cannot be parsed
93
     */
94
    public static Date parseDateTime(String dateTime) {
95 1 1. parseDateTime : negated conditional → KILLED
        if (dateTime == null) {
96
            throw new IllegalArgumentException("dateTime must not be null");
97
        }
98
        DateFormat dateFormat = new SimpleDateFormat(DATE_TIME_FORMAT);
99
        try {
100 1 1. parseDateTime : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::parseDateTime → KILLED
            return dateFormat.parse(dateTime);
101
        } catch (ParseException e) {
102
            throw new IllegalArgumentException("Unparseable date/time: " + dateTime, e);
103
        }
104
    }
105
106
    /**
107
     * Gives the current date and time in the UTC time zone.
108
     *
109
     * @return the current UTC date and time
110
     */
111
    public static ZonedDateTime utcNow() {
112 1 1. utcNow : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::utcNow → KILLED
        return ZonedDateTime.now(ZoneOffset.UTC);
113
    }
114
115
    /**
116
     * Writes a string to a file using the given character encoding.
117
     *
118
     * @param s        the string to write
119
     * @param filename the name of the file to write to
120
     * @param charset  the character set to use, e.g., {@code java.nio.charset.StandardCharsets.UTF_8}
121
     *
122
     * @throws IOException if writing to the file failed
123
     */
124
    @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Use at your own risk")
125
    public static void writeToFile(String s, String filename, Charset charset) throws IOException {
126
        try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(filename), charset)) {
127 1 1. writeToFile : removed call to java/io/BufferedWriter::write → KILLED
            writer.write(s);
128
        }
129
    }
130
131
    /**
132
     * Reads a string from a classpath resource, which is assumed to be UTF-8 encoded text.
133
     *
134
     * @param resourceName the name of the classpath resource to read
135
     *
136
     * @return a string representation of the classpath resource {@code resourceName}
137
     *
138
     * @throws IOException if reading the resource failed
139
     */
140
    public static String readResource(String resourceName) throws IOException {
141 1 1. readResource : negated conditional → KILLED
        if (resourceName == null) {
142
            throw new IllegalArgumentException("resourceName must not be null");
143
        }
144
        StringBuilder sb = new StringBuilder();
145
        try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) {
146 1 1. readResource : negated conditional → KILLED
            if (is == null) {
147
                throw new IllegalArgumentException("Resource not found: " + resourceName);
148
            }
149
            try (Scanner s = new Scanner(is, StandardCharsets.UTF_8)) {
150 1 1. readResource : negated conditional → KILLED
                while (s.hasNextLine()) {
151
                    sb.append(s.nextLine()).append(System.lineSeparator());
152
                }
153 1 1. readResource : replaced return value with "" for com/reallifedeveloper/tools/test/TestUtil::readResource → KILLED
                return sb.toString();
154
            }
155
        }
156
    }
157
158
    /**
159
     * Injects a value into an object's field, which may be private.
160
     *
161
     * @param obj       the object in which to inject the value
162
     * @param fieldName the name of the field
163
     * @param value     the value to inject, may be {@code null}
164
     *
165
     * @throws IllegalArgumentException if {@code obj} or {@code fieldName} is {@code null}
166
     * @throws IllegalStateException    if reflecction failure
167
     */
168
    @SuppressWarnings("PMD.AvoidAccessibilityAlteration")
169
    public static void injectField(Object obj, String fieldName, @Nullable Object value) {
170
        try {
171
            Field field = getField(obj, fieldName);
172 1 1. injectField : removed call to java/lang/reflect/Field::setAccessible → KILLED
            field.setAccessible(true);
173 1 1. injectField : removed call to java/lang/reflect/Field::set → KILLED
            field.set(obj, value);
174
        } catch (ReflectiveOperationException e) {
175
            throw new IllegalStateException("Error injecting " + value + " into field " + fieldName + " of object " + obj, e);
176
        }
177
    }
178
179
    /**
180
     * Gives the value of an object's field, which may be private.
181
     * <p>
182
     * This also works with nested fields, so if {@code fieldName} is {@code "a.b.c"}, the method does something like the following:
183
     *
184
     * <pre>
185
     * Object temp1 = obj.a;
186
     * Object temp2 = temp1.b;
187
     * Object result = temp2.c;
188
     * return result;
189
     * </pre>
190
     *
191
     * For nested fields, the method is "forgiving", in that if some intermediate field is {@code null}, the method returns {@code null}
192
     * instead of throwing an exception. So in the example above, if {@code temp1.b} is {@code null}, the method would return {@code null}
193
     * after the second step.
194
     *
195
     * @param obj       the object containing the field
196
     * @param fieldName the name of the field, may be nested, e.g., {@code "field1.nestedField"}
197
     *
198
     * @return the value of the field {@code fieldName} in the object {@code obj}
199
     *
200
     * @throws IllegalArgumentException if {@code obj} or {@code fieldName} is {@code null}
201
     * @throws IllegalStateException    if reflection failure
202
     */
203
    @SuppressWarnings("PMD.AvoidAccessibilityAlteration")
204
    public static @Nullable Object getFieldValue(Object obj, String fieldName) {
205 2 1. getFieldValue : negated conditional → KILLED
2. getFieldValue : negated conditional → KILLED
        if (obj == null || fieldName == null) {
206
            throw new IllegalArgumentException("Arguments must not be null: obj=%s, fieldName=%s".formatted(obj, fieldName));
207
        }
208
        Object fieldValue = obj;
209
        for (String singleFieldName : fieldName.split("\\.", -1)) {
210
            try {
211
                Field field = getField(fieldValue, singleFieldName);
212 1 1. getFieldValue : removed call to java/lang/reflect/Field::setAccessible → KILLED
                field.setAccessible(true);
213
                fieldValue = field.get(fieldValue);
214 1 1. getFieldValue : negated conditional → KILLED
                if (fieldValue == null) {
215
                    break;
216
                }
217
            } catch (ReflectiveOperationException e) {
218
                throw new IllegalStateException("Error getting value of field " + singleFieldName + " of object " + fieldValue, e);
219
            }
220
        }
221 1 1. getFieldValue : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::getFieldValue → KILLED
        return fieldValue;
222
    }
223
224
    private static Field getField(Object obj, String fieldName) throws NoSuchFieldException {
225 2 1. getField : negated conditional → KILLED
2. getField : negated conditional → KILLED
        if (obj == null || fieldName == null) {
226
            throw new IllegalArgumentException("Arguments must not be null: obj=%s, fieldName=%s".formatted(obj, fieldName));
227
        }
228
        Class<?> entityType = obj.getClass();
229 1 1. getField : negated conditional → KILLED
        while (entityType != null) {
230
            for (Field field : entityType.getDeclaredFields()) {
231 1 1. getField : negated conditional → KILLED
                if (field.getName().equals(fieldName)) {
232 1 1. getField : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::getField → KILLED
                    return field;
233
                }
234
            }
235
            entityType = entityType.getSuperclass();
236
        }
237
        throw new NoSuchFieldException(fieldName);
238
    }
239
240
    /**
241
     * Checks that a value that is declared as {@code @Nullable} actually is non-null.
242
     *
243
     * @param <T> the type of {@code x}
244
     *
245
     * @param x   the value to check
246
     *
247
     * @return {@code x} if it is non-null
248
     *
249
     * @throws IllegalStateException if {@code x} is {@code null}
250
     *
251
     * @see <a href="https://github.com/uber/NullAway/wiki/Suppressing-Warnings">The NullAway documentation</a>
252
     */
253
    public static <T> T castToNonNull(@Nullable T x) {
254 1 1. castToNonNull : negated conditional → KILLED
        if (x == null) {
255
            throw new IllegalStateException("Expected value to be non-null");
256
        }
257 1 1. castToNonNull : replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::castToNonNull → KILLED
        return x;
258
    }
259
260
    /**
261
     * A null-safe version of {@code java.util.Arrays.asList} that converts an array to a list, or returns null if the array is null.
262
     *
263
     * @param <T> the type of the array
264
     * @param a   the array
265
     *
266
     * @return the array as a list, or {@code null} if {@code a} is {@code null}
267
     */
268
    @SuppressWarnings({ "checkstyle:noReturnNull", "PMD.UseVarargs", "PMD.ReturnEmptyCollectionRatherThanNull" })
269
    public static <T> @Nullable List<T> asList(@Nullable T[] a) {
270 1 1. asList : negated conditional → NO_COVERAGE
        if (a == null) {
271 1 1. asList : replaced return value with Collections.emptyList for com/reallifedeveloper/tools/test/TestUtil::asList → NO_COVERAGE
            return null;
272
        } else {
273 1 1. asList : replaced return value with Collections.emptyList for com/reallifedeveloper/tools/test/TestUtil::asList → NO_COVERAGE
            return Arrays.asList(a);
274
        }
275
    }
276
}

Mutations

60

1.1
Location : findFreePort
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:findFreePort()]
replaced int return with 0 for com/reallifedeveloper/tools/test/TestUtil::findFreePort → KILLED

74

1.1
Location : parseDate
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:parseNullDate()]
negated conditional → KILLED

79

1.1
Location : parseDate
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:parseDate()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::parseDate → KILLED

95

1.1
Location : parseDateTime
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:parseMalformedDateTime()]
negated conditional → KILLED

100

1.1
Location : parseDateTime
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:parseDateTime()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::parseDateTime → KILLED

112

1.1
Location : utcNow
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:utcNowShouldReturnCurrentDateAndTimeInUtc()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::utcNow → KILLED

127

1.1
Location : writeToFile
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:writeToFile()]
removed call to java/io/BufferedWriter::write → KILLED

141

1.1
Location : readResource
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:readResourceNullResourceName()]
negated conditional → KILLED

146

1.1
Location : readResource
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:readNonExistingResource()]
negated conditional → KILLED

150

1.1
Location : readResource
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:readResource()]
negated conditional → KILLED

153

1.1
Location : readResource
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:readResource()]
replaced return value with "" for com/reallifedeveloper/tools/test/TestUtil::readResource → KILLED

172

1.1
Location : injectField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInSubClass()]
removed call to java/lang/reflect/Field::setAccessible → KILLED

173

1.1
Location : injectField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInSubClass()]
removed call to java/lang/reflect/Field::set → KILLED

205

1.1
Location : getFieldValue
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:getNestedFieldValue()]
negated conditional → KILLED

2.2
Location : getFieldValue
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:getValueOfNullFieldName()]
negated conditional → KILLED

212

1.1
Location : getFieldValue
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:getNestedFieldValue()]
removed call to java/lang/reflect/Field::setAccessible → KILLED

214

1.1
Location : getFieldValue
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:getNestedFieldValue()]
negated conditional → KILLED

221

1.1
Location : getFieldValue
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:getNestedFieldValue()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::getFieldValue → KILLED

225

1.1
Location : getField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldWithNullFieldName()]
negated conditional → KILLED

2.2
Location : getField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInNullObject()]
negated conditional → KILLED

229

1.1
Location : getField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInSubClass()]
negated conditional → KILLED

231

1.1
Location : getField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInSubClass()]
negated conditional → KILLED

232

1.1
Location : getField
Killed by : com.reallifedeveloper.tools.test.TestUtilTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.TestUtilTest]/[method:injectFieldInSubClass()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::getField → KILLED

254

1.1
Location : castToNonNull
Killed by : com.reallifedeveloper.tools.test.database.dbunit.DbUnitFlatXmlReaderTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.database.dbunit.DbUnitFlatXmlReaderTest]/[method:readFileWithoutIdAttributeAndNoPrimaryKeyGenerator()]
negated conditional → KILLED

257

1.1
Location : castToNonNull
Killed by : com.reallifedeveloper.tools.test.database.dbunit.DbUnitFlatXmlReaderTest.[engine:junit-jupiter]/[class:com.reallifedeveloper.tools.test.database.dbunit.DbUnitFlatXmlReaderTest]/[method:readFileWithoutIdAttributeAndNoPrimaryKeyGenerator()]
replaced return value with null for com/reallifedeveloper/tools/test/TestUtil::castToNonNull → KILLED

270

1.1
Location : asList
Killed by : none
negated conditional → NO_COVERAGE

271

1.1
Location : asList
Killed by : none
replaced return value with Collections.emptyList for com/reallifedeveloper/tools/test/TestUtil::asList → NO_COVERAGE

273

1.1
Location : asList
Killed by : none
replaced return value with Collections.emptyList for com/reallifedeveloper/tools/test/TestUtil::asList → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.23.0