DbFileConverter.java

1
package com.renomad.minum.database;
2
3
import com.renomad.minum.logging.ILogger;
4
import com.renomad.minum.state.Context;
5
import com.renomad.minum.utils.IFileUtils;
6
7
import java.io.BufferedReader;
8
import java.io.FileReader;
9
import java.io.IOException;
10
import java.nio.file.Path;
11
import java.text.ParseException;
12
import java.util.ArrayList;
13
import java.util.Comparator;
14
import java.util.List;
15
import java.util.stream.Stream;
16
17
import static com.renomad.minum.utils.Invariants.mustBeFalse;
18
import static java.nio.charset.StandardCharsets.US_ASCII;
19
import static java.nio.file.StandardOpenOption.*;
20
21
/**
22
 * This class exists to handle converting from one file/folder
23
 * style of database to another.
24
 */
25
final class DbFileConverter {
26
27
    private final Path dbDirectory;
28
    private final ILogger logger;
29
    private final DatabaseAppender databaseAppender;
30
    private final DatabaseConsolidator databaseConsolidator;
31
    private final IFileUtils fileUtils;
32
33
    /**
34
     * Construct a converter instance
35
     * @param context this is used for its constants, logger, and so on.
36
     * @param dbDirectory this is the specific directory for this database,
37
     *                    for example, the full path to the "users" database directory.
38
     */
39
    DbFileConverter(Context context, Path dbDirectory, IFileUtils fileUtils) {
40
        this.dbDirectory = dbDirectory;
41
        this.logger = context.getLogger();
42
        this.fileUtils = fileUtils;
43
        try {
44
            this.databaseAppender = new DatabaseAppender(dbDirectory, context, fileUtils);
45
            this.databaseConsolidator = new DatabaseConsolidator(dbDirectory, context, fileUtils);
46
        } catch (IOException ex) {
47
            throw new DbException("Error in DbFileConverter constructor", ex);
48
        }
49
50
    }
51
52
    /**
53
     * convert a directory of database files from the old Db classic form to the DbEngine2 form.
54
     * The old form was one file per data item, the new form has append-only
55
     * data logs and consolidated files.
56
     */
57
    void convertClassicFolderStructureToDbEngine2Form() throws IOException {
58 1 1. convertClassicFolderStructureToDbEngine2Form : removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingClassicToDbEngine2 → TIMED_OUT
        displayWarningConvertingClassicToDbEngine2();
59
        try (var fileReader = new FileReader(dbDirectory.resolve("index.ddps").toFile(), US_ASCII);
60
             BufferedReader br = new BufferedReader(fileReader)) {
61
            String s = br.readLine();
62 1 1. convertClassicFolderStructureToDbEngine2Form : negated conditional → KILLED
            if (s == null)
63
                throw new DbException("index file for " + dbDirectory + " returned null when reading a line from it");
64
            mustBeFalse(s.isBlank(), "Unless something is terribly broken, we expect a numeric value here");
65
        }
66
67 1 1. convertClassicFolderStructureToDbEngine2Form : removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbToDbEngine2 → TIMED_OUT
        walkFilesAndConvertDbToDbEngine2(this.dbDirectory, this.logger, fileUtils);
68
    }
69
70
71
72
    private void displayWarningConvertingClassicToDbEngine2() {
73
        logger.logDebug(() -> "*****************************************************************");
74
        logger.logDebug(() -> "*****************************************************************");
75
        logger.logDebug(() -> "........");
76
        logger.logDebug(() -> "About to convert database files from Db Classic to Db Engine 2");
77
        logger.logDebug(() -> "........");
78
        logger.logDebug(() -> "*****************************************************************");
79
        logger.logDebug(() -> "*****************************************************************");
80
    }
81
82
    private void displayWarningConvertingDbEngine2ToClassic() {
83
        logger.logDebug(() -> "*****************************************************************");
84
        logger.logDebug(() -> "*****************************************************************");
85
        logger.logDebug(() -> "........");
86
        logger.logDebug(() -> "About to convert database files from Db Engine2 to Db Classic");
87
        logger.logDebug(() -> "........");
88
        logger.logDebug(() -> "*****************************************************************");
89
        logger.logDebug(() -> "*****************************************************************");
90
    }
91
92
    /**
93
     * walk through all the files in this directory, collecting
94
     * all regular files (non-subdirectories) except for index.ddps
95
     */
96
    static void walkFilesAndConvertDbToDbEngine2(Path dbDirectory, ILogger logger, IFileUtils fileUtils) throws IOException {
97
        List<Path> listOfFiles = getListOfFiles(dbDirectory, fileUtils);
98
        // convert each file to the new database schema by appending it
99
        // to the append log, and then delete it
100 2 1. walkFilesAndConvertDbToDbEngine2 : changed conditional boundary → TIMED_OUT
2. walkFilesAndConvertDbToDbEngine2 : negated conditional → KILLED
        for (int i = 0; i < listOfFiles.size(); i++) {
101 1 1. walkFilesAndConvertDbToDbEngine2 : Replaced integer division with multiplication → TIMED_OUT
            int percentCompletion = listOfFiles.size() / 100;
102 1 1. walkFilesAndConvertDbToDbEngine2 : negated conditional → TIMED_OUT
            if (i == percentCompletion) {
103
                logger.logDebug(() -> "File converting is %d percent complete".formatted(percentCompletion));
104
            }
105
            Path p = extractDataAndAppend(dbDirectory, logger, listOfFiles.get(i), fileUtils);
106 1 1. walkFilesAndConvertDbToDbEngine2 : removed call to com/renomad/minum/utils/IFileUtils::delete → TIMED_OUT
            fileUtils.delete(p);
107
        }
108
109
        // at this point, after all the ordinary files have been removed, kill the index file
110 1 1. walkFilesAndConvertDbToDbEngine2 : removed call to com/renomad/minum/utils/IFileUtils::delete → KILLED
        fileUtils.delete(dbDirectory.resolve("index.ddps"));
111
    }
112
113
    /**
114
     * This method digs into the Db Classic file, checks everything is kosher, and
115
     * if so, appends it to the append-only log file which is part of DbEngine2
116
     */
117
    static Path extractDataAndAppend(Path dbDirectory, ILogger logger, Path fileToAnalyze, IFileUtils fileUtils) throws IOException {
118
        String fileContents = checkFileDetailsAreValid(fileToAnalyze, logger, fileUtils);
119 1 1. extractDataAndAppend : negated conditional → KILLED
        if (!fileContents.isBlank()) {
120 1 1. extractDataAndAppend : removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED
            fileUtils.writeString(
121
                dbDirectory.resolve("currentAppendLog"),
122
                "UPDATE %s\n".formatted(fileContents), APPEND, CREATE);
123
        }
124 1 1. extractDataAndAppend : replaced return value with null for com/renomad/minum/database/DbFileConverter::extractDataAndAppend → KILLED
        return fileToAnalyze;
125
    }
126
127
    /**
128
     * Get the files that make up the file schema of Db Classic
129
     */
130
    private static List<Path> getListOfFiles(Path dbDirectory, IFileUtils fileUtils) throws IOException {
131
        List<Path> listOfFiles;
132
        try (Stream<Path> fileStream = fileUtils.list(dbDirectory)) {
133
            listOfFiles = fileStream.filter(path ->
134 2 1. lambda$getListOfFiles$15 : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$getListOfFiles$15 → TIMED_OUT
2. lambda$getListOfFiles$15 : negated conditional → KILLED
                            fileUtils.isRegularFile(path) &&
135 1 1. lambda$getListOfFiles$15 : negated conditional → TIMED_OUT
                                    path.getFileName().toString().endsWith(".ddps") &&
136 1 1. lambda$getListOfFiles$15 : negated conditional → KILLED
                                    !path.getFileName().toString().startsWith("index"))
137
                    .toList();
138
        }
139 1 1. getListOfFiles : replaced return value with Collections.emptyList for com/renomad/minum/database/DbFileConverter::getListOfFiles → TIMED_OUT
        return listOfFiles;
140
    }
141
142
    /**
143
     * This code inspects that the data in the file is valid
144
     * and returns the data. This method is called as part of
145
     * convert Db Classic to DbEngine2
146
     */
147
    static String checkFileDetailsAreValid(Path p, ILogger logger, IFileUtils fileUtils) throws IOException {
148 1 1. checkFileDetailsAreValid : negated conditional → KILLED
        if (!fileUtils.isRegularFile(p)) {
149
            throw new DbException("At checkFileDetailsAreValid, path " + p + " is not a regular file");
150
        }
151
        String fileName = p.getFileName().toString();
152
        int startOfSuffixIndex = fileName.indexOf('.');
153
        String fileContents = fileUtils.readString(p);
154 1 1. checkFileDetailsAreValid : negated conditional → KILLED
        if (fileContents.isBlank()) {
155
            logger.logDebug( () -> fileName + " file exists but empty, skipping");
156
            return "";
157
        } else {
158 1 1. checkFileDetailsAreValid : replaced return value with "" for com/renomad/minum/database/DbFileConverter::checkFileDetailsAreValid → KILLED
            return deserializeDataFromDbFile(fileName, startOfSuffixIndex, fileContents);
159
        }
160
    }
161
162
    /**
163
     * While converting Db Classic files to DbEngine2, each data file will be inspected.
164
     * Here, we are looking at the contents of the files.
165
     */
166
    static String deserializeDataFromDbFile(String filename, int startOfSuffixIndex, String fileContents) {
167
        int fileNameIdentifier = Integer.parseInt(filename.substring(0, startOfSuffixIndex));
168
        int indexOfFirstPipe = fileContents.indexOf('|');
169
        String indexString = fileContents.substring(0, indexOfFirstPipe);
170
        long index = Long.parseLong(indexString);
171 1 1. deserializeDataFromDbFile : negated conditional → KILLED
        if (index != fileNameIdentifier) {
172
            throw new DbException( "The filename (%s) must correspond to the index in its contents (%d)"
173
                    .formatted(filename, index));
174
        }
175 1 1. deserializeDataFromDbFile : replaced return value with "" for com/renomad/minum/database/DbFileConverter::deserializeDataFromDbFile → KILLED
        return fileContents;
176
    }
177
178
    /**
179
     * Convert the folder/file structure.  From DbEngine2 format to Db classic.
180
     */
181
    void convertFolderStructureToDbClassic() throws IOException, ParseException {
182 1 1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingDbEngine2ToClassic → TIMED_OUT
        displayWarningConvertingDbEngine2ToClassic();
183
184
        // if there are any remnant items in the current append-only file, move them
185
        // to a new file
186
        databaseAppender.saveOffCurrentDataToReadyFolder();
187
188
        // consolidate whatever files still exist in the append logs
189 1 1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED
        databaseConsolidator.consolidate();
190
191
        // at this point, all the data is consolidated, in order, so we can step
192
        // through the data, creating new files, and finish up with an index.ddps
193
        // set to the proper value (i.e. one greater than the max index in the database)
194 1 1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbEngine2ToDbClassic → TIMED_OUT
        walkFilesAndConvertDbEngine2ToDbClassic(this.dbDirectory, this.logger, fileUtils);
195
    }
196
197
    /**
198
     * This is a pretty intricate method.  It basically steps through the consolidated
199
     * data files, writing each line to a new file.  It has to handle this through
200
     * multiple files and deleting files when finished, and closing file handles
201
     * appropriately when done with a file, and doing it in the proper order.
202
     */
203
    static void walkFilesAndConvertDbEngine2ToDbClassic(Path dbDirectory, ILogger logger, IFileUtils fileUtils) throws IOException {
204
        // get the list of consolidated data files
205
        List<Path> listOfFiles;
206
        try (Stream<Path> fileStream = fileUtils.list(dbDirectory.resolve("consolidated_data"))) {
207 2 1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 : replaced boolean return with false for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → TIMED_OUT
2. lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → KILLED
            listOfFiles = new ArrayList<>(fileStream.filter(x -> DbFileConverter.isDataFile(x, fileUtils)).toList());
208
        }
209
210
        // sort the data in ascending order.  Files like "1_to_10" will come before "11_to_20"
211
        listOfFiles.sort(Comparator.comparing(x -> {
212
            String filename = x.getFileName().toString();
213
            int indexOfFirstUnderscore = filename.indexOf('_');
214 1 1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 : negated conditional → TIMED_OUT
            if (indexOfFirstUnderscore == -1) {
215
                throw new DbException("Error: Failed to find first underscore in filename: " + filename);
216
            }
217
            String firstIndexNumberOfFile = filename.substring(0, indexOfFirstUnderscore);
218
            try {
219 1 1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 : replaced Long return value with 0L for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 → KILLED
                return Long.parseLong(firstIndexNumberOfFile);
220
            } catch (NumberFormatException ex) {
221
                throw new DbException("Failed to convert first part of filename to a number: " + filename);
222
            }
223
        }));
224
225
        // initialize a variable to record the current maximum index value
226
        long currentMaxIndexValue = -1;
227
228
        // initialize a variable to record the count of data items converted to Db Classic
229
        long countConvertedFiles = 0;
230
231
        // convert each line of each file to its own file, per the needs of Db Classic
232
        for (Path filePath : listOfFiles) {
233
            try (BufferedReader reader = fileUtils.newBufferedReader(filePath.toFile().toPath(), US_ASCII)) {
234
                String line;
235
                while ((line = reader.readLine()) != null) {
236
                    int i = line.indexOf('|');
237
                    if (i == -1) {
238
                        throw new DbException(("Unable to convert a line - check for " +
239
                                "corruption.  File: %s Data: %s").formatted(filePath, line));
240
                    }
241
                    String indexNumberString = line.substring(0, i);
242
                    long indexNumber;
243
                    try {
244
                        indexNumber = Long.parseLong(indexNumberString);
245
                        currentMaxIndexValue = Math.max(indexNumber, currentMaxIndexValue);
246
                    } catch (NumberFormatException ex) {
247
                        throw new DbException(("Unable to convert a line - check for " +
248
                                "corruption.  File: %s Data: %s").formatted(filePath, line));
249
                    }
250
                    String dbFilename = indexNumber + ".ddps";
251
                    Path dbFullPath = dbDirectory.resolve(dbFilename);
252
                    fileUtils.writeString(dbFullPath, line, CREATE, WRITE);
253
                    countConvertedFiles += 1;
254
                    logAlongConversion(logger, countConvertedFiles, 1000);
255
                }
256
                // now we've converted everything from this file, delete it and its checksum (if available)
257
                fileUtils.delete(filePath);
258
                Path checksumPath = filePath.resolveSibling(filePath.getFileName() + ".checksum");
259
                fileUtils.deleteIfExists(checksumPath);
260
            }
261
        }
262
263 1 1. walkFilesAndConvertDbEngine2ToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::deleteEmptyDbEngine2Directories → KILLED
        deleteEmptyDbEngine2Directories(dbDirectory, fileUtils);
264 2 1. walkFilesAndConvertDbEngine2ToDbClassic : Replaced long addition with subtraction → TIMED_OUT
2. walkFilesAndConvertDbEngine2ToDbClassic : removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED
        fileUtils.writeString(dbDirectory.resolve("index.ddps"), String.valueOf(currentMaxIndexValue + 1), CREATE_NEW);
265
    }
266
267
    /**
268
     * A predicate used to filter for just regular database files, not checksum files
269
     */
270
    static boolean isDataFile(Path path, IFileUtils fileUtils) {
271 3 1. isDataFile : negated conditional → TIMED_OUT
2. isDataFile : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::isDataFile → TIMED_OUT
3. isDataFile : negated conditional → KILLED
        return fileUtils.isRegularFile(path) && !path.toString().contains("checksum");
272
    }
273
274
    /**
275
     * This method is called after conversion to Db classic, at which point these directories
276
     * will be empty.
277
     */
278
    static void deleteEmptyDbEngine2Directories(Path dbDirectory, IFileUtils fileUtils) throws IOException {
279
        fileUtils.deleteIfExists(dbDirectory.resolve("consolidated_data"));
280
        fileUtils.deleteIfExists(dbDirectory.resolve("currentAppendLog"));
281
        fileUtils.deleteIfExists(dbDirectory.resolve("append_logs"));
282
    }
283
284
    /**
285
     * A helper to choose when to output logging statements during the conversion
286
     * of files from one database file format to another.
287
     * @param countConvertedFilesModulo modulo this number at which a log statement will be output.
288
     */
289
    static void logAlongConversion(ILogger logger, long countConvertedFiles, int countConvertedFilesModulo) {
290 2 1. logAlongConversion : Replaced long modulus with multiplication → KILLED
2. logAlongConversion : negated conditional → KILLED
        if (countConvertedFiles % countConvertedFilesModulo == 0) {
291
            logger.logDebug(() -> "DbFileConverter has converted %d files to Db Classic form"
292
                    .formatted(countConvertedFiles));
293
        }
294
    }
295
}

Mutations

58

1.1
Location : convertClassicFolderStructureToDbEngine2Form
Killed by : none
removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingClassicToDbEngine2 → TIMED_OUT

62

1.1
Location : convertClassicFolderStructureToDbEngine2Form
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

67

1.1
Location : convertClassicFolderStructureToDbEngine2Form
Killed by : none
removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbToDbEngine2 → TIMED_OUT

100

1.1
Location : walkFilesAndConvertDbToDbEngine2
Killed by : none
changed conditional boundary → TIMED_OUT

2.2
Location : walkFilesAndConvertDbToDbEngine2
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

101

1.1
Location : walkFilesAndConvertDbToDbEngine2
Killed by : none
Replaced integer division with multiplication → TIMED_OUT

102

1.1
Location : walkFilesAndConvertDbToDbEngine2
Killed by : none
negated conditional → TIMED_OUT

106

1.1
Location : walkFilesAndConvertDbToDbEngine2
Killed by : none
removed call to com/renomad/minum/utils/IFileUtils::delete → TIMED_OUT

110

1.1
Location : walkFilesAndConvertDbToDbEngine2
Killed by : com.renomad.minum.database.DbFileConverterTests
removed call to com/renomad/minum/utils/IFileUtils::delete → KILLED

119

1.1
Location : extractDataAndAppend
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

120

1.1
Location : extractDataAndAppend
Killed by : com.renomad.minum.database.DbFileConverterTests
removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED

124

1.1
Location : extractDataAndAppend
Killed by : com.renomad.minum.database.DbEngine2Tests
replaced return value with null for com/renomad/minum/database/DbFileConverter::extractDataAndAppend → KILLED

134

1.1
Location : lambda$getListOfFiles$15
Killed by : com.renomad.minum.database.DbEngine2Tests
negated conditional → KILLED

2.2
Location : lambda$getListOfFiles$15
Killed by : none
replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$getListOfFiles$15 → TIMED_OUT

135

1.1
Location : lambda$getListOfFiles$15
Killed by : none
negated conditional → TIMED_OUT

136

1.1
Location : lambda$getListOfFiles$15
Killed by : com.renomad.minum.database.DbEngine2Tests
negated conditional → KILLED

139

1.1
Location : getListOfFiles
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/database/DbFileConverter::getListOfFiles → TIMED_OUT

148

1.1
Location : checkFileDetailsAreValid
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

154

1.1
Location : checkFileDetailsAreValid
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

158

1.1
Location : checkFileDetailsAreValid
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced return value with "" for com/renomad/minum/database/DbFileConverter::checkFileDetailsAreValid → KILLED

171

1.1
Location : deserializeDataFromDbFile
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

175

1.1
Location : deserializeDataFromDbFile
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced return value with "" for com/renomad/minum/database/DbFileConverter::deserializeDataFromDbFile → KILLED

182

1.1
Location : convertFolderStructureToDbClassic
Killed by : none
removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingDbEngine2ToClassic → TIMED_OUT

189

1.1
Location : convertFolderStructureToDbClassic
Killed by : com.renomad.minum.database.DbFileConverterTests
removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED

194

1.1
Location : convertFolderStructureToDbClassic
Killed by : none
removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbEngine2ToDbClassic → TIMED_OUT

207

1.1
Location : lambda$walkFilesAndConvertDbEngine2ToDbClassic$17
Killed by : com.renomad.minum.database.DbTests
replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → KILLED

2.2
Location : lambda$walkFilesAndConvertDbEngine2ToDbClassic$17
Killed by : none
replaced boolean return with false for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → TIMED_OUT

214

1.1
Location : lambda$walkFilesAndConvertDbEngine2ToDbClassic$18
Killed by : none
negated conditional → TIMED_OUT

219

1.1
Location : lambda$walkFilesAndConvertDbEngine2ToDbClassic$18
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced Long return value with 0L for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 → KILLED

263

1.1
Location : walkFilesAndConvertDbEngine2ToDbClassic
Killed by : com.renomad.minum.database.DbTests
removed call to com/renomad/minum/database/DbFileConverter::deleteEmptyDbEngine2Directories → KILLED

264

1.1
Location : walkFilesAndConvertDbEngine2ToDbClassic
Killed by : com.renomad.minum.database.DbTests
removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED

2.2
Location : walkFilesAndConvertDbEngine2ToDbClassic
Killed by : none
Replaced long addition with subtraction → TIMED_OUT

271

1.1
Location : isDataFile
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : isDataFile
Killed by : none
replaced boolean return with true for com/renomad/minum/database/DbFileConverter::isDataFile → TIMED_OUT

3.3
Location : isDataFile
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

290

1.1
Location : logAlongConversion
Killed by : com.renomad.minum.database.DbFileConverterTests
Replaced long modulus with multiplication → KILLED

2.2
Location : logAlongConversion
Killed by : com.renomad.minum.database.DbFileConverterTests
negated conditional → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0