mainframe-migration-toolkit 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. mainframe_migration_toolkit-0.2.0.dist-info/METADATA +16 -0
  2. mainframe_migration_toolkit-0.2.0.dist-info/RECORD +52 -0
  3. mainframe_migration_toolkit-0.2.0.dist-info/WHEEL +4 -0
  4. mainframe_migration_toolkit-0.2.0.dist-info/entry_points.txt +3 -0
  5. mainframe_toolkit/__init__.py +92 -0
  6. mainframe_toolkit/__main__.py +5 -0
  7. mainframe_toolkit/_workspace/.claude/skills/analyze-mainframe-similarity/SKILL.md +30 -0
  8. mainframe_toolkit/_workspace/.claude/skills/migrate-mainframe-job/SKILL.md +63 -0
  9. mainframe_toolkit/_workspace/.claude/skills/validate-golden-dataset/SKILL.md +12 -0
  10. mainframe_toolkit/_workspace/AGENTS.md +10 -0
  11. mainframe_toolkit/_workspace/CLAUDE.md +2 -0
  12. mainframe_toolkit/_workspace/validator-java/.mvn/wrapper/maven-wrapper.properties +3 -0
  13. mainframe_toolkit/_workspace/validator-java/mvnw +295 -0
  14. mainframe_toolkit/_workspace/validator-java/mvnw.cmd +189 -0
  15. mainframe_toolkit/_workspace/validator-java/pom.xml +116 -0
  16. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/AvroValueFormatter.java +152 -0
  17. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/CsvTabularReader.java +111 -0
  18. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/DataFormat.java +62 -0
  19. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/Difference.java +34 -0
  20. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/InputFileSet.java +45 -0
  21. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/InputOptions.java +23 -0
  22. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/JsonReportWriter.java +40 -0
  23. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/MultiFileTabularReader.java +80 -0
  24. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/Normalization.java +16 -0
  25. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ParquetTabularReader.java +61 -0
  26. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/TabularReader.java +13 -0
  27. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/TabularReaderFactory.java +29 -0
  28. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationOptions.java +40 -0
  29. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationReport.java +44 -0
  30. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationService.java +395 -0
  31. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidatorCli.java +190 -0
  32. mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValueNormalizer.java +27 -0
  33. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/DirectoryValidationTest.java +70 -0
  34. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/JsonReportWriterTest.java +43 -0
  35. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/KeyedValidationTest.java +89 -0
  36. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ParquetValidationTest.java +146 -0
  37. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ValidationServiceTest.java +137 -0
  38. mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ValidatorCliTest.java +102 -0
  39. mainframe_toolkit/cli.py +361 -0
  40. mainframe_toolkit/cobol.py +106 -0
  41. mainframe_toolkit/copybook.py +558 -0
  42. mainframe_toolkit/errors.py +15 -0
  43. mainframe_toolkit/external.py +48 -0
  44. mainframe_toolkit/io.py +202 -0
  45. mainframe_toolkit/jcl.py +126 -0
  46. mainframe_toolkit/pipeline.py +322 -0
  47. mainframe_toolkit/sequential.py +259 -0
  48. mainframe_toolkit/similarity.py +1171 -0
  49. mainframe_toolkit/sorting.py +60 -0
  50. mainframe_toolkit/specs.py +312 -0
  51. mainframe_toolkit/synthetic.py +108 -0
  52. mainframe_toolkit/workspace.py +133 -0
@@ -0,0 +1,43 @@
1
+ package io.mainframe.migration.validator;
2
+
3
+ import org.junit.jupiter.api.Test;
4
+
5
+ import java.io.StringWriter;
6
+ import java.util.List;
7
+
8
+ import static org.junit.jupiter.api.Assertions.assertEquals;
9
+ import static org.junit.jupiter.api.Assertions.assertFalse;
10
+ import static org.junit.jupiter.api.Assertions.assertTrue;
11
+
12
+ class JsonReportWriterTest {
13
+ @Test
14
+ void outputIsStableAndUsesLfOnEveryPlatform() throws Exception {
15
+ ValidationReport report = new ValidationReport(
16
+ "DIFFERENT",
17
+ "expected.csv",
18
+ "actual.csv",
19
+ "CSV",
20
+ "CSV",
21
+ List.of(),
22
+ List.of("trim"),
23
+ 1,
24
+ 1,
25
+ 1,
26
+ 1,
27
+ 1,
28
+ 1,
29
+ false,
30
+ List.of(new Difference(1, 1, "name", " Ana ", "Bia", "VALUE_MISMATCH")));
31
+ JsonReportWriter writer = new JsonReportWriter();
32
+ StringWriter first = new StringWriter();
33
+ StringWriter second = new StringWriter();
34
+
35
+ writer.write(report, first);
36
+ writer.write(report, second);
37
+
38
+ assertEquals(first.toString(), second.toString());
39
+ assertFalse(first.toString().contains("\r"));
40
+ assertTrue(first.toString().endsWith("\n"));
41
+ assertTrue(first.toString().contains("\"line\" : 1"));
42
+ }
43
+ }
@@ -0,0 +1,89 @@
1
+ package io.mainframe.migration.validator;
2
+
3
+ import org.junit.jupiter.api.Test;
4
+ import org.junit.jupiter.api.io.TempDir;
5
+
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.nio.file.Files;
8
+ import java.nio.file.Path;
9
+ import java.util.List;
10
+ import java.util.Set;
11
+
12
+ import static org.junit.jupiter.api.Assertions.assertEquals;
13
+ import static org.junit.jupiter.api.Assertions.assertTrue;
14
+
15
+ class KeyedValidationTest {
16
+ @TempDir
17
+ Path temporaryDirectory;
18
+
19
+ @Test
20
+ void comparesRowsByCompositeKeyRegardlessOfPhysicalOrder() throws Exception {
21
+ Path expected = csv("expected.csv", "account,seq,value\nB,2,x\nA,1,y\n");
22
+ Path actual = csv("actual.csv", "account,seq,value\nA,1,y\nB,2,x\n");
23
+
24
+ ValidationReport report = validate(expected, actual, List.of("account", "seq"), Set.of());
25
+
26
+ assertTrue(report.matches());
27
+ assertEquals(List.of("account", "seq"), report.keys());
28
+ assertEquals(2, report.expectedRows());
29
+ }
30
+
31
+ @Test
32
+ void ordersDifferencesByNormalizedKey() throws Exception {
33
+ Path expected = csv("expected.csv", "id,value\nB,expected-b\n A ,expected-a\n");
34
+ Path actual = csv("actual.csv", "id,value\nA,actual-a\nB,actual-b\n");
35
+
36
+ ValidationReport report = validate(
37
+ expected, actual, List.of("id"), Set.of(Normalization.TRIM));
38
+
39
+ assertEquals(2, report.differenceCount());
40
+ assertEquals(" A ", report.differences().get(0).key().get("id"));
41
+ assertEquals("B", report.differences().get(1).key().get("id"));
42
+ assertEquals("value", report.differences().get(0).column());
43
+ }
44
+
45
+ @Test
46
+ void reportsDuplicateKeysOnEitherSide() throws Exception {
47
+ Path expected = csv("expected.csv", "id,value\n1,A\n1,A\n2,B\n");
48
+ Path actual = csv("actual.csv", "id,value\n2,B\n1,A\n1,A\n");
49
+
50
+ ValidationReport report = validate(expected, actual, List.of("id"), Set.of());
51
+
52
+ assertEquals(2, report.differenceCount());
53
+ assertEquals("DUPLICATE_EXPECTED_KEY", report.differences().get(0).reason());
54
+ assertEquals("DUPLICATE_ACTUAL_KEY", report.differences().get(1).reason());
55
+ assertEquals("1", report.differences().get(0).key().get("id"));
56
+ }
57
+
58
+ @Test
59
+ void keyModeWorksAcrossSparkPartDirectories() throws Exception {
60
+ Path expected = Files.createDirectory(temporaryDirectory.resolve("expected-dir"));
61
+ Files.writeString(expected.resolve("part-00000.csv"), "id,value\n2,B\n");
62
+ Files.writeString(expected.resolve("part-00001.csv"), "id,value\n1,A\n");
63
+ Path actual = Files.createDirectory(temporaryDirectory.resolve("actual-dir"));
64
+ Files.writeString(actual.resolve("part-00000.csv"), "id,value\n1,A\n2,B\n");
65
+
66
+ ValidationReport report = validate(expected, actual, List.of("id"), Set.of());
67
+
68
+ assertTrue(report.matches());
69
+ }
70
+
71
+ private ValidationReport validate(
72
+ Path expected,
73
+ Path actual,
74
+ List<String> keys,
75
+ Set<Normalization> normalizations) throws Exception {
76
+ InputOptions left = new InputOptions(
77
+ expected, DataFormat.AUTO, ',', true, StandardCharsets.UTF_8);
78
+ InputOptions right = new InputOptions(
79
+ actual, DataFormat.AUTO, ',', true, StandardCharsets.UTF_8);
80
+ return new ValidationService().validate(
81
+ new ValidationOptions(left, right, normalizations, 100, keys));
82
+ }
83
+
84
+ private Path csv(String name, String content) throws Exception {
85
+ Path path = temporaryDirectory.resolve(name);
86
+ Files.writeString(path, content, StandardCharsets.UTF_8);
87
+ return path;
88
+ }
89
+ }
@@ -0,0 +1,146 @@
1
+ package io.mainframe.migration.validator;
2
+
3
+ import org.apache.avro.Schema;
4
+ import org.apache.avro.generic.GenericData;
5
+ import org.apache.avro.generic.GenericRecord;
6
+ import org.apache.parquet.avro.AvroParquetWriter;
7
+ import org.apache.parquet.hadoop.ParquetWriter;
8
+ import org.apache.parquet.hadoop.metadata.CompressionCodecName;
9
+ import org.apache.parquet.io.LocalOutputFile;
10
+ import org.junit.jupiter.api.Test;
11
+ import org.junit.jupiter.api.io.TempDir;
12
+
13
+ import java.nio.charset.StandardCharsets;
14
+ import java.nio.file.Files;
15
+ import java.nio.file.Path;
16
+ import java.util.List;
17
+ import java.util.Set;
18
+
19
+ import static org.junit.jupiter.api.Assertions.assertEquals;
20
+ import static org.junit.jupiter.api.Assertions.assertTrue;
21
+
22
+ class ParquetValidationTest {
23
+ private static final Schema SCHEMA = new Schema.Parser().parse("""
24
+ {
25
+ "type": "record",
26
+ "name": "Result",
27
+ "fields": [
28
+ {"name": "id", "type": "long"},
29
+ {"name": "amount", "type": "double"},
30
+ {"name": "name", "type": ["null", "string"], "default": null}
31
+ ]
32
+ }
33
+ """);
34
+
35
+ @TempDir
36
+ Path temporaryDirectory;
37
+
38
+ @Test
39
+ void comparesParquetCellsInRecordOrder() throws Exception {
40
+ Path expected = parquet("expected.parquet", List.of(
41
+ row(1, 10.5, "Ana"),
42
+ row(2, 20.0, "Bia")));
43
+ Path actual = parquet("actual.parquet", List.of(
44
+ row(1, 10.5, "Ana"),
45
+ row(2, 20.0, "Bea")));
46
+
47
+ ValidationReport report = validate(
48
+ input(expected, DataFormat.AUTO, true),
49
+ input(actual, DataFormat.AUTO, true),
50
+ Set.of());
51
+
52
+ assertEquals(1, report.differenceCount());
53
+ Difference difference = report.differences().get(0);
54
+ assertEquals(2, difference.line());
55
+ assertEquals(3, difference.columnIndex());
56
+ assertEquals("name", difference.column());
57
+ assertEquals("Bia", difference.expected());
58
+ assertEquals("Bea", difference.actual());
59
+ }
60
+
61
+ @Test
62
+ void readsSchemaFromEmptyParquetFiles() throws Exception {
63
+ Path expected = parquet("expected.parquet", List.of());
64
+ Path actual = parquet("actual.parquet", List.of());
65
+
66
+ ValidationReport report = validate(
67
+ input(expected, DataFormat.PARQUET, true),
68
+ input(actual, DataFormat.PARQUET, true),
69
+ Set.of());
70
+
71
+ assertTrue(report.matches());
72
+ assertEquals(0, report.expectedRows());
73
+ assertEquals(3, report.expectedColumns());
74
+ }
75
+
76
+ @Test
77
+ void comparesCsvAndParquetWithDecimalNormalization() throws Exception {
78
+ Path expected = temporaryDirectory.resolve("expected.csv");
79
+ Files.writeString(expected, "id,amount,name\n1,1.2300,Ana\n", StandardCharsets.UTF_8);
80
+ Path actual = parquet("actual.parquet", List.of(row(1, 1.23, "Ana")));
81
+
82
+ ValidationReport report = validate(
83
+ input(expected, DataFormat.CSV, true),
84
+ input(actual, DataFormat.PARQUET, true),
85
+ Set.of(Normalization.DECIMAL));
86
+
87
+ assertTrue(report.matches());
88
+ }
89
+
90
+ @Test
91
+ void readsSparkParquetPartDirectoriesInLexicalOrder() throws Exception {
92
+ Path expected = Files.createDirectory(temporaryDirectory.resolve("expected-dir"));
93
+ parquet(expected.resolve("part-00001.snappy.parquet"), List.of(row(2, 2.0, "Bia")));
94
+ parquet(expected.resolve("part-00000.snappy.parquet"), List.of(row(1, 1.0, "Ana")));
95
+ Files.writeString(expected.resolve("_SUCCESS"), "");
96
+ Path actual = Files.createDirectory(temporaryDirectory.resolve("actual-dir"));
97
+ parquet(actual.resolve("part-00000.parquet"), List.of(
98
+ row(1, 1.0, "Ana"),
99
+ row(2, 2.0, "Bia")));
100
+
101
+ ValidationReport report = validate(
102
+ input(expected, DataFormat.AUTO, true),
103
+ input(actual, DataFormat.AUTO, true),
104
+ Set.of());
105
+
106
+ assertTrue(report.matches());
107
+ assertEquals(2, report.expectedRows());
108
+ assertEquals("PARQUET", report.expectedFormat());
109
+ }
110
+
111
+ private ValidationReport validate(
112
+ InputOptions expected,
113
+ InputOptions actual,
114
+ Set<Normalization> normalizations) throws Exception {
115
+ return new ValidationService().validate(new ValidationOptions(expected, actual, normalizations, 100));
116
+ }
117
+
118
+ private InputOptions input(Path path, DataFormat format, boolean header) {
119
+ return new InputOptions(path, format, ',', header, StandardCharsets.UTF_8);
120
+ }
121
+
122
+ private GenericRecord row(long id, double amount, String name) {
123
+ GenericRecord record = new GenericData.Record(SCHEMA);
124
+ record.put("id", id);
125
+ record.put("amount", amount);
126
+ record.put("name", name);
127
+ return record;
128
+ }
129
+
130
+ private Path parquet(String name, List<GenericRecord> records) throws Exception {
131
+ Path path = temporaryDirectory.resolve(name);
132
+ return parquet(path, records);
133
+ }
134
+
135
+ private Path parquet(Path path, List<GenericRecord> records) throws Exception {
136
+ try (ParquetWriter<GenericRecord> writer = AvroParquetWriter.<GenericRecord>builder(new LocalOutputFile(path))
137
+ .withSchema(SCHEMA)
138
+ .withCompressionCodec(CompressionCodecName.UNCOMPRESSED)
139
+ .build()) {
140
+ for (GenericRecord record : records) {
141
+ writer.write(record);
142
+ }
143
+ }
144
+ return path;
145
+ }
146
+ }
@@ -0,0 +1,137 @@
1
+ package io.mainframe.migration.validator;
2
+
3
+ import org.junit.jupiter.api.Test;
4
+ import org.junit.jupiter.api.io.TempDir;
5
+
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.nio.file.Files;
8
+ import java.nio.file.Path;
9
+ import java.util.Set;
10
+
11
+ import static org.junit.jupiter.api.Assertions.assertEquals;
12
+ import static org.junit.jupiter.api.Assertions.assertFalse;
13
+ import static org.junit.jupiter.api.Assertions.assertNull;
14
+ import static org.junit.jupiter.api.Assertions.assertTrue;
15
+
16
+ class ValidationServiceTest {
17
+ @TempDir
18
+ Path temporaryDirectory;
19
+
20
+ @Test
21
+ void reportsEveryCellWithItsLineAndColumn() throws Exception {
22
+ Path expected = csv("expected.csv", "id;amount\n1;10.00\n2;ABC\n");
23
+ Path actual = csv("actual.csv", "id;amount\n1;10.01\n2;ABC\n3;X\n");
24
+
25
+ ValidationReport report = validate(expected, actual, ';', true, Set.of(), 20);
26
+
27
+ assertEquals("DIFFERENT", report.status());
28
+ assertEquals(2, report.expectedRows());
29
+ assertEquals(3, report.actualRows());
30
+ assertEquals(3, report.differenceCount());
31
+ Difference first = report.differences().get(0);
32
+ assertEquals(1, first.line());
33
+ assertEquals(2, first.columnIndex());
34
+ assertEquals("amount", first.column());
35
+ assertEquals("10.00", first.expected());
36
+ assertEquals("10.01", first.actual());
37
+ assertEquals("VALUE_MISMATCH", first.reason());
38
+ Difference missingRowCell = report.differences().get(1);
39
+ assertEquals(3, missingRowCell.line());
40
+ assertNull(missingRowCell.expected());
41
+ assertEquals("3", missingRowCell.actual());
42
+ assertEquals("UNEXPECTED_ACTUAL_CELL", missingRowCell.reason());
43
+ }
44
+
45
+ @Test
46
+ void comparesNamedColumnsBeforeData() throws Exception {
47
+ Path expected = csv("expected.csv", "id,name\n1,Ana\n");
48
+ Path actual = csv("actual.csv", "id,full_name\n1,Ana\n");
49
+
50
+ ValidationReport report = validate(expected, actual, ',', true, Set.of(), 20);
51
+
52
+ assertEquals(1, report.differenceCount());
53
+ Difference difference = report.differences().get(0);
54
+ assertEquals(0, difference.line());
55
+ assertEquals(2, difference.columnIndex());
56
+ assertEquals("name", difference.expected());
57
+ assertEquals("full_name", difference.actual());
58
+ assertEquals("COLUMN_NAME_MISMATCH", difference.reason());
59
+ }
60
+
61
+ @Test
62
+ void appliesTrimAndDecimalNormalizations() throws Exception {
63
+ Path expected = csv("expected.csv", "value\n 001.2300 \n-0.00\ntext \n");
64
+ Path actual = csv("actual.csv", "value\n1.23\n0\ntext\n");
65
+
66
+ ValidationReport report = validate(
67
+ expected,
68
+ actual,
69
+ ',',
70
+ true,
71
+ Set.of(Normalization.TRIM, Normalization.DECIMAL),
72
+ 20);
73
+
74
+ assertTrue(report.matches());
75
+ assertEquals("MATCH", report.status());
76
+ assertEquals(0, report.differenceCount());
77
+ assertEquals(java.util.List.of("trim", "decimal"), report.normalizations());
78
+ }
79
+
80
+ @Test
81
+ void noHeaderKeepsFirstRecordAsData() throws Exception {
82
+ Path expected = csv("expected.csv", "1,Ana\n2,Bia\n");
83
+ Path actual = csv("actual.csv", "1,Ana\n2,Bia\n");
84
+
85
+ ValidationReport report = validate(expected, actual, ',', false, Set.of(), 20);
86
+
87
+ assertTrue(report.matches());
88
+ assertEquals(2, report.expectedRows());
89
+ assertEquals(2, report.expectedColumns());
90
+ }
91
+
92
+ @Test
93
+ void limitsDetailsButCountsAllDifferences() throws Exception {
94
+ Path expected = csv("expected.csv", "a,b\n1,2\n3,4\n");
95
+ Path actual = csv("actual.csv", "a,b\nx,y\nz,w\n");
96
+
97
+ ValidationReport report = validate(expected, actual, ',', true, Set.of(), 2);
98
+
99
+ assertEquals(4, report.differenceCount());
100
+ assertEquals(2, report.reportedDifferenceCount());
101
+ assertEquals(2, report.differences().size());
102
+ assertTrue(report.truncated());
103
+ }
104
+
105
+ @Test
106
+ void leavesNonNumericCellsUntouchedDuringDecimalNormalization() throws Exception {
107
+ Path expected = csv("expected.csv", "v\n001A\n");
108
+ Path actual = csv("actual.csv", "v\n1A\n");
109
+
110
+ ValidationReport report = validate(
111
+ expected, actual, ',', true, Set.of(Normalization.DECIMAL), 20);
112
+
113
+ assertFalse(report.matches());
114
+ assertEquals("001A", report.differences().get(0).expected());
115
+ }
116
+
117
+ private ValidationReport validate(
118
+ Path expected,
119
+ Path actual,
120
+ char delimiter,
121
+ boolean header,
122
+ Set<Normalization> normalizations,
123
+ int maxDifferences) throws Exception {
124
+ InputOptions expectedOptions = new InputOptions(
125
+ expected, DataFormat.AUTO, delimiter, header, StandardCharsets.UTF_8);
126
+ InputOptions actualOptions = new InputOptions(
127
+ actual, DataFormat.AUTO, delimiter, header, StandardCharsets.UTF_8);
128
+ return new ValidationService().validate(
129
+ new ValidationOptions(expectedOptions, actualOptions, normalizations, maxDifferences));
130
+ }
131
+
132
+ private Path csv(String name, String contents) throws Exception {
133
+ Path path = temporaryDirectory.resolve(name);
134
+ Files.writeString(path, contents, StandardCharsets.UTF_8);
135
+ return path;
136
+ }
137
+ }
@@ -0,0 +1,102 @@
1
+ package io.mainframe.migration.validator;
2
+
3
+ import org.junit.jupiter.api.Test;
4
+ import org.junit.jupiter.api.io.TempDir;
5
+ import picocli.CommandLine;
6
+
7
+ import java.io.PrintWriter;
8
+ import java.io.StringWriter;
9
+ import java.nio.file.Files;
10
+ import java.nio.file.Path;
11
+
12
+ import static org.junit.jupiter.api.Assertions.assertEquals;
13
+ import static org.junit.jupiter.api.Assertions.assertTrue;
14
+
15
+ class ValidatorCliTest {
16
+ @TempDir
17
+ Path temporaryDirectory;
18
+
19
+ @Test
20
+ void returnsZeroForMatchAndWritesJsonToStdout() throws Exception {
21
+ Path expected = write("expected.csv", "v\n 01.00 \n");
22
+ Path actual = write("actual.csv", "v\n1\n");
23
+ StringWriter stdout = new StringWriter();
24
+ CommandLine command = ValidatorCli.commandLine();
25
+ command.setOut(new PrintWriter(stdout, true));
26
+
27
+ int exitCode = command.execute(
28
+ expected.toString(),
29
+ actual.toString(),
30
+ "--normalize", "trim,decimal");
31
+
32
+ assertEquals(ValidatorCli.EXIT_MATCH, exitCode);
33
+ assertTrue(stdout.toString().contains("\"status\" : \"MATCH\""));
34
+ }
35
+
36
+ @Test
37
+ void returnsOneForDifferencesAndWritesReportFile() throws Exception {
38
+ Path expected = write("expected.data", "id|name\n1|Ana\n");
39
+ Path actual = write("actual.data", "id|name\n1|Bia\n");
40
+ Path report = temporaryDirectory.resolve("reports/result.json");
41
+ CommandLine command = ValidatorCli.commandLine();
42
+
43
+ int exitCode = command.execute(
44
+ expected.toString(),
45
+ actual.toString(),
46
+ "--format", "csv",
47
+ "--delimiter", "pipe",
48
+ "--report", report.toString());
49
+
50
+ assertEquals(ValidatorCli.EXIT_DIFFERENT, exitCode);
51
+ assertTrue(Files.readString(report).contains("\"status\" : \"DIFFERENT\""));
52
+ }
53
+
54
+ @Test
55
+ void returnsTwoForInvalidInput() {
56
+ StringWriter stderr = new StringWriter();
57
+ CommandLine command = ValidatorCli.commandLine();
58
+ command.setErr(new PrintWriter(stderr, true));
59
+
60
+ int exitCode = command.execute("missing.csv", "also-missing.csv");
61
+
62
+ assertEquals(ValidatorCli.EXIT_INPUT_ERROR, exitCode);
63
+ assertTrue(stderr.toString().contains("Arquivo ou diretório não encontrado"));
64
+ }
65
+
66
+ @Test
67
+ void supportsNoHeaderAndTabAlias() throws Exception {
68
+ Path expected = write("expected.csv", "1\tAna\n");
69
+ Path actual = write("actual.csv", "1\tAna\n");
70
+ StringWriter stdout = new StringWriter();
71
+ CommandLine command = ValidatorCli.commandLine();
72
+ command.setOut(new PrintWriter(stdout, true));
73
+
74
+ int exitCode = command.execute(
75
+ expected.toString(), actual.toString(), "--delimiter", "tab", "--no-header");
76
+
77
+ assertEquals(ValidatorCli.EXIT_MATCH, exitCode);
78
+ assertTrue(stdout.toString().contains("\"expectedRows\" : 1"), stdout::toString);
79
+ }
80
+
81
+ @Test
82
+ void acceptsRepeatedAndCommaSeparatedKeys() throws Exception {
83
+ Path expected = write("expected.csv", "tenant,id,value\nB,2,x\nA,1,y\n");
84
+ Path actual = write("actual.csv", "tenant,id,value\nA,1,y\nB,2,x\n");
85
+ StringWriter stdout = new StringWriter();
86
+ CommandLine command = ValidatorCli.commandLine();
87
+ command.setOut(new PrintWriter(stdout, true));
88
+
89
+ int exitCode = command.execute(
90
+ expected.toString(), actual.toString(), "--key", "tenant,id");
91
+
92
+ assertEquals(ValidatorCli.EXIT_MATCH, exitCode);
93
+ assertTrue(stdout.toString().contains("\"tenant\""), stdout::toString);
94
+ assertTrue(stdout.toString().contains("\"id\""), stdout::toString);
95
+ }
96
+
97
+ private Path write(String name, String content) throws Exception {
98
+ Path path = temporaryDirectory.resolve(name);
99
+ Files.writeString(path, content);
100
+ return path;
101
+ }
102
+ }