Skip to content

Commit 2107584

Browse files
Implement diffing algorithm for snapshot tests of outcome printer. (rescript-lang#67)
Minimal implementation of the unix diff tool based on https://en.wikipedia.org/wiki/Longest_common_subsequence_problem and https://en.wikipedia.org/wiki/Diff
1 parent bbf0e7e commit 2107584

File tree

7 files changed

+160
-7
lines changed

7 files changed

+160
-7
lines changed

syntax/.depend

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ src/napkin_scanner.cmx : src/napkin_token.cmx src/napkin_diagnostics.cmx \
7575
src/napkin_scanner.cmi
7676
src/napkin_scanner.cmi : src/napkin_token.cmx src/napkin_diagnostics.cmi
7777
src/napkin_token.cmx : src/napkin_comment.cmx src/napkin_character_codes.cmx
78+
tests/napkin_diff.cmx : tests/napkin_diff.cmi
79+
tests/napkin_diff.cmi :
7880
tests/napkin_test.cmx : src/napkin_token.cmx src/napkin_parser.cmx \
7981
src/napkin_outcome_printer.cmx src/napkin_multi_printer.cmx \
80-
src/napkin_io.cmx src/napkin_driver.cmx
82+
src/napkin_io.cmx src/napkin_driver.cmx tests/napkin_diff.cmx

syntax/Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
OCAMLOPT=ocamlopt.opt
2-
OCAMLFLAGS= -w +a-4-42-40-9-48 -warn-error +a -bin-annot -I +compiler-libs -I src
2+
OCAMLFLAGS= -w +a-4-42-40-9-48 -warn-error +a -bin-annot -I +compiler-libs -I src -I tests
33
OCAMLDEP=ocamldep.opt
44
%.cmi : %.mli
55
$(OCAMLOPT) $(OCAMLFLAGS) -c $<
@@ -37,6 +37,9 @@ FILES = \
3737
src/napkin_outcome_printer.cmx \
3838
src/napkin_multi_printer.cmx
3939

40+
TEST_FILES = \
41+
tests/napkin_diff.cmx
42+
4043
.DEFAULT_GOAL := build-native
4144
build-native: lib/refmt.exe $(FILES) src/napkin_main.cmx depend
4245
$(OCAMLOPT) $(OCAMLFLAGS) -O2 -o ./lib/napkinscript.exe -I +compiler-libs ocamlcommon.cmxa -I src $(FILES) src/napkin_main.cmx
@@ -59,8 +62,8 @@ lib/bench.exe: benchmarks/refmt_main3b.cmx benchmarks/Benchmark.ml $(FILES)
5962
benchmarks/refmt_main3b.cmx: benchmarks/refmt_main3b.ml
6063
$(OCAMLOPT) -c -O2 -I +compiler-libs ocamlcommon.cmxa benchmarks/refmt_main3b.ml
6164

62-
lib/test.exe: tests/napkin_test.cmx
63-
$(OCAMLOPT) $(OCAMLFLAGS) -O2 -o ./lib/test.exe -bin-annot -I +compiler-libs ocamlcommon.cmxa -I src $(FILES) tests/napkin_test.ml
65+
lib/test.exe: $(TEST_FILES) tests/napkin_test.cmx depend
66+
$(OCAMLOPT) $(OCAMLFLAGS) -O2 -o ./lib/test.exe -bin-annot -I +compiler-libs ocamlcommon.cmxa -I src -I tests $(FILES) $(TEST_FILES) tests/napkin_test.cmx
6467

6568
test: build-native lib/test.exe
6669
./node_modules/.bin/jest

syntax/tests/napkin_diff.ml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
type change = {
2+
(* number of lines in new data changed/inserted here. *)
3+
inserted: int;
4+
(* number of lines in old data changed/deleted here. *)
5+
deleted: int;
6+
(* line number of 1st deleted line. *)
7+
firstDeletedLine: int;
8+
(* line number of 1st inserted line. *)
9+
firstInsertedLine: int;
10+
}
11+
12+
(* A diff is a chain of changes *)
13+
type diff = change list
14+
15+
(* 'a' stands for added, 'd' for deleted and 'c' for changed *)
16+
let changeLetter inserts deletes =
17+
match inserts, deletes with
18+
| 0, _ -> 'd'
19+
| _, 0 -> 'a'
20+
| _ -> 'c'
21+
22+
let translateLineNumber lnum = lnum + 1
23+
24+
let numberRange a b =
25+
let a = translateLineNumber a in
26+
let b = translateLineNumber b in
27+
if b > a then
28+
Format.sprintf "%d,%d" a b
29+
else
30+
string_of_int b
31+
32+
let printChange change oldArr newArr =
33+
(* No insertions or deletions means same line *)
34+
if change.inserted = 0 && change.deleted == 0 then ()
35+
else
36+
let lastDeletedLine = (change.firstDeletedLine + change.deleted - 1) in
37+
let lastInsertedLine = (change.firstInsertedLine + change.inserted - 1) in
38+
Format.sprintf "%s%c%s"
39+
(numberRange change.firstDeletedLine lastDeletedLine)
40+
(changeLetter change.inserted change.deleted)
41+
(numberRange change.firstInsertedLine lastInsertedLine)
42+
|> print_endline;
43+
44+
if change.deleted > 0 then
45+
for i = change.firstDeletedLine to lastDeletedLine do
46+
Format.sprintf "< %s" (Array.get oldArr i) |> print_endline;
47+
done;
48+
if change.deleted > 0 && change.inserted > 0 then
49+
print_endline "---";
50+
if change.inserted > 0 then
51+
for i = change.firstInsertedLine to lastInsertedLine do
52+
Format.sprintf "> %s" (Array.get newArr i) |> print_endline;
53+
done
54+
55+
(* Compute longest common subsequence between oldArr and newArr
56+
* Uses the dynamic programming approach described on:
57+
* https://en.wikipedia.org/wiki/Longest_common_subsequence_problem *)
58+
let computeMatrix ~compareFn oldArr oldLen newArr newLen =
59+
let matrix = Array.make_matrix (oldLen + 1) (newLen + 1) 0 in
60+
61+
for i = 1 to oldLen do
62+
for j = 1 to newLen do
63+
let c = compareFn (Array.unsafe_get oldArr (i - 1)) (Array.unsafe_get newArr (j - 1)) in
64+
if c = 0 then
65+
let northEast = Array.unsafe_get (Array.unsafe_get matrix (i - 1)) (j - 1) in
66+
Array.unsafe_set (Array.unsafe_get matrix i) j (northEast + 1)
67+
else
68+
let north = Array.unsafe_get (Array.unsafe_get matrix (i - 1)) j in
69+
let east = Array.unsafe_get (Array.unsafe_get matrix i) (j - 1) in
70+
Array.unsafe_set (Array.unsafe_get matrix i) j (max east north)
71+
done;
72+
done;
73+
74+
matrix
75+
76+
(* emits differences between `oldArr` and `newArr` through the `onChange` callback *)
77+
let diff ~onChange ~compareFn oldArr newArr =
78+
let oldLen = Array.length oldArr in
79+
let newLen = Array.length newArr in
80+
81+
(* compute the longest common subsequence *)
82+
let matrix = computeMatrix ~compareFn oldArr oldLen newArr newLen in
83+
84+
(* Read the longest common subsequence matrix and compute changes while working
85+
* backwards through the matrix *)
86+
let rec walkMatrix i j firstDeletedLine firstInsertedLine =
87+
if i > 0 && j > 0 then (
88+
if (Array.unsafe_get oldArr (i - 1)) = (Array.unsafe_get newArr (j - 1)) then (
89+
onChange {
90+
firstDeletedLine = i;
91+
firstInsertedLine = j;
92+
deleted = firstDeletedLine - i;
93+
inserted = firstInsertedLine - j;
94+
};
95+
let nextI = i - 1 in
96+
let nextJ = j - 1 in
97+
walkMatrix nextI nextJ nextI nextJ
98+
) else if (Array.unsafe_get (Array.unsafe_get matrix (i - 1)) j) >= (Array.unsafe_get (Array.unsafe_get matrix i) (j - 1)) then (
99+
walkMatrix (i - 1) j firstDeletedLine firstInsertedLine
100+
) else (
101+
walkMatrix i (j - 1) firstDeletedLine firstInsertedLine
102+
)
103+
) else if j > 0 && i = 0 then (
104+
walkMatrix i (j - 1) firstDeletedLine firstInsertedLine
105+
) else if i > 0 && j = 0 then (
106+
walkMatrix (i - 1) j firstDeletedLine firstInsertedLine
107+
) else (
108+
onChange {
109+
firstDeletedLine = i;
110+
firstInsertedLine = j;
111+
deleted = firstDeletedLine - i;
112+
inserted = firstInsertedLine - j
113+
}
114+
)
115+
in
116+
walkMatrix oldLen newLen oldLen newLen
117+
118+
(* splits both strings in lines and does a line-based diff *)
119+
let diffTwoStrings oldString newString =
120+
let oldArr = oldString |> String.split_on_char '\n' |> Array.of_list in
121+
let newArr = newString |> String.split_on_char '\n' |> Array.of_list in
122+
let script = ref [] in
123+
diff
124+
~onChange:(fun change -> script.contents <- change::script.contents)
125+
~compareFn:String.compare
126+
oldArr
127+
newArr;
128+
List.iter (fun change -> printChange change oldArr newArr) script.contents

syntax/tests/napkin_diff.mli

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(* data comparison utilities
2+
* compute and display difference between two pieces of data *)
3+
4+
(* represents a place where some lines are deleted and some are inserted *)
5+
type change
6+
7+
(* the result of a comparison is a diff; a chain of changes *)
8+
type diff = change list
9+
10+
val diff:
11+
onChange:(change -> unit)
12+
-> compareFn:('data -> 'data -> int)
13+
-> 'data array
14+
-> 'data array
15+
-> unit
16+
17+
(* compute and print line-based diff between two strings *)
18+
val diffTwoStrings: string -> string -> unit

syntax/tests/napkin_test.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ module Snapshot = struct
1616
) else (
1717
(* snapshot file exists *)
1818
let snapContents = IO.readFile ~filename:snapFilename in
19-
(* poor man's diff algorithm *)
19+
(* check for equality, do diffing later if needed *)
2020
if contents = snapContents then None else Some snapContents
2121
)
2222
in
2323
match diff with
24-
| Some contents ->
24+
| Some snapContents ->
2525
prerr_string ("❌ snapshot " ^ filename);
2626
prerr_newline();
27-
prerr_string contents;
27+
Napkin_diff.diffTwoStrings snapContents contents;
2828
exit 1
2929
| None ->
3030
print_endline (

syntax/tests/oprint/oprint.res

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
let name = "Steve"
22
let x = 42
3+
let pi = 3.14
34

45
let numbersArray = [1, 2, 3, 4, 5]
56
let numbersTuple = (1, 2, 3, 4, 5)

syntax/tests/oprint/oprint.res.snapshot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
let name: string
22
let x: int
3+
let pi: float
34
let numbersArray: array<int>
45
let numbersTuple: (int, int, int, int, int)
56
let numbersList: list<int>

0 commit comments

Comments
 (0)