From 49b4a26a9358191aabd3b847d2861578d5f8bd7b Mon Sep 17 00:00:00 2001 From: Martino Luca Date: Thu, 19 Apr 2018 10:00:24 -0700 Subject: [PATCH] New module FileDiff to compute relevant lines on a modified file Summary: Given a difference between two files, return the relevant lines in the new file; a line is relevant when a change took place in it, or nearby. To generate a valid input for this parser, use unix-diff command with the following formatter arguments: ``` diff --unchanged-line-format="U" --old-line-format="O" --new-line-format="N" File1 File2 ``` Reviewed By: ezgicicek Differential Revision: D7385267 fbshipit-source-id: 0cc3143 --- infer/src/base/FileDiff.ml | 90 +++++++++++ infer/src/base/FileDiff.mli | 30 ++++ infer/src/unit/FileDiffTests.ml | 267 ++++++++++++++++++++++++++++++++ infer/src/unit/inferunit.ml | 1 + 4 files changed, 388 insertions(+) create mode 100644 infer/src/base/FileDiff.ml create mode 100644 infer/src/base/FileDiff.mli create mode 100644 infer/src/unit/FileDiffTests.ml diff --git a/infer/src/base/FileDiff.ml b/infer/src/base/FileDiff.ml new file mode 100644 index 000000000..5659a524d --- /dev/null +++ b/infer/src/base/FileDiff.ml @@ -0,0 +1,90 @@ +(* + * Copyright (c) 2018 - present Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + *) + +open! IStd + +module UnixDiff = struct + type t = Unchanged | New | Old [@@deriving compare] + + let equal = [%compare.equal : t] + + let directive_of_char c = + match c with + | 'U' -> + Unchanged + | 'N' -> + New + | 'O' -> + Old + | _ -> + Logging.die Logging.UserError "Unexpected char in input sequence. Failed parsing" + + + let process_raw_directives in_str = + if String.is_empty in_str then [] else String.to_list in_str |> List.map ~f:directive_of_char + + + let pp fmt d = + match d with + | Unchanged -> + Format.fprintf fmt "U" + | New -> + Format.fprintf fmt "N" + | Old -> + Format.fprintf fmt "O" + + + module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY = struct + type nonrec t = t = Unchanged | New | Old + + let equal = equal + + let process_raw_directives = process_raw_directives + + let pp = pp + end +end + +let parse_directives directives = + let rec aux ~d ~lines ~line_ptr ~pred_is_old = + (* O does not move the line-pointer *) + (* N moves the line-pointer and marks the line as affected *) + (* U moves the line-pointer, and marks the line as affected ONLY if it is preceded by O *) + match d with + | [] -> + List.rev lines + | UnixDiff.Old :: ds -> + aux ~d:ds ~lines ~line_ptr ~pred_is_old:true + | UnixDiff.New :: ds -> + aux ~d:ds ~lines:(line_ptr :: lines) ~line_ptr:(line_ptr + 1) ~pred_is_old:false + | UnixDiff.Unchanged :: ds -> + let lines' = if pred_is_old then line_ptr :: lines else lines in + aux ~d:ds ~lines:lines' ~line_ptr:(line_ptr + 1) ~pred_is_old:false + in + if List.is_empty directives then (* handle the case where both files are empty *) + [] + else if (* handle the case where the new-file is empty *) + List.for_all ~f:(UnixDiff.equal UnixDiff.Old) directives + then [1] + else + let pred_is_old, directives' = + match directives with UnixDiff.Old :: ds -> (true, ds) | _ -> (false, directives) + in + aux ~d:directives' ~lines:[] ~line_ptr:1 ~pred_is_old + + +module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY = struct + let parse_directives = parse_directives +end + +(** Given a difference between two files, return the relevant lines in the new file; a line is + relevant when a change took place in it, or nearby. To generate a valid input for this + parser, use unix-diff command with the following formatter arguments: + diff --unchanged-line-format="U" --old-line-format="O" --new-line-format="N" File1 File2 *) +let parse_unix_diff str = UnixDiff.process_raw_directives str |> parse_directives diff --git a/infer/src/base/FileDiff.mli b/infer/src/base/FileDiff.mli new file mode 100644 index 000000000..e8f3de9ba --- /dev/null +++ b/infer/src/base/FileDiff.mli @@ -0,0 +1,30 @@ +(* + * Copyright (c) 2018 - present Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + *) + +module UnixDiff : sig + module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY : sig + type t = Unchanged | New | Old + + val equal : t -> t -> bool + + val process_raw_directives : string -> t list + + val pp : Format.formatter -> t -> unit + end +end + +module VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY : sig + val parse_directives : UnixDiff.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY.t list -> int list +end + +val parse_unix_diff : string -> int list +(** Given a difference between two files, return the relevant lines in the new file; a line is + relevant when a change took place in it, or nearby. To generate a valid input for this + parser, use unix-diff command with the following formatter arguments: + diff --unchanged-line-format="U" --old-line-format="O" --new-line-format="N" File1 File2 *) diff --git a/infer/src/unit/FileDiffTests.ml b/infer/src/unit/FileDiffTests.ml new file mode 100644 index 000000000..5d99d68b4 --- /dev/null +++ b/infer/src/unit/FileDiffTests.ml @@ -0,0 +1,267 @@ +(* + * Copyright (c) 2018 - present Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + *) + +open! IStd +open OUnit2 +module UnixDiffTest = FileDiff.UnixDiff.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY + +let test_unixdiff_process_raw_directives_with_valid_input = + let create_test input expected _ = + let found = UnixDiffTest.process_raw_directives input in + let pp_diff fmt (expected, actual) = + let expected_str = Format.asprintf "%a" (Pp.seq ~sep:"" UnixDiffTest.pp) expected in + let actual_str = Format.asprintf "%a" (Pp.seq ~sep:"" UnixDiffTest.pp) actual in + Format.fprintf fmt "Expected: '%s', found: '%s'" expected_str actual_str + in + assert_equal ~cmp:(List.equal ~equal:UnixDiffTest.equal) ~pp_diff expected found + in + [ ( "test_unixdiff_process_raw_directives_1" + , "UOOU" + , UnixDiffTest.([Unchanged; Old; Old; Unchanged]) ) + ; ("test_unixdiff_process_raw_directives_2", "", []) ] + |> List.map ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output ) + + +let test_unixdiff_process_raw_directives_with_invalid_input = + let create_test input expected_exception _ = + let run () = UnixDiffTest.process_raw_directives input in + assert_raises expected_exception run + in + [ ( "test_unixdiff_process_raw_directives_1" + , "U OOU" + , Logging.InferUserError "Unexpected char in input sequence. Failed parsing" ) + ; ( "test_unixdiff_process_raw_directives_2" + , "UZ" + , Logging.InferUserError "Unexpected char in input sequence. Failed parsing" ) + ; ( "test_unixdiff_process_raw_directives_3" + , "UU " + , Logging.InferUserError "Unexpected char in input sequence. Failed parsing" ) + ; ( "test_unixdiff_process_raw_directives_4" + , " U" + , Logging.InferUserError "Unexpected char in input sequence. Failed parsing" ) ] + |> List.map ~f:(fun (name, test_input, expected_exception) -> + name >:: create_test test_input expected_exception ) + + +let test_unixdiff_pp = + let create_test input expected _ = + let found = Format.asprintf "%a" (Pp.seq ~sep:"" UnixDiffTest.pp) input in + let pp_diff fmt (expected, actual) = + Format.fprintf fmt "Expected: '%s', found: '%s'" expected actual + in + assert_equal ~cmp:String.equal ~pp_diff expected found + in + [ ("test_unixdiff_pp_1", UnixDiffTest.([Unchanged; Old; Old; Unchanged]), "UOOU") + ; ("test_unixdiff_pp_2", [], "") ] + |> List.map ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output ) + + +module FileDiffTest = FileDiff.VISIBLE_FOR_TESTING_DO_NOT_USE_DIRECTLY + +let u length = List.init ~f:(fun _ -> UnixDiffTest.Unchanged) length + +let n length = List.init ~f:(fun _ -> UnixDiffTest.New) length + +let o length = List.init ~f:(fun _ -> UnixDiffTest.Old) length + +let test_parse_directives_with_valid_input = + let create_test input expected _ = + let found = FileDiffTest.parse_directives input in + let pp_diff fmt (expected, actual) = + let expected_str = Format.asprintf "%a" (Pp.seq ~sep:", " Format.pp_print_int) expected in + let actual_str = Format.asprintf "%a" (Pp.seq ~sep:", " Format.pp_print_int) actual in + Format.fprintf fmt "Expected: '%s', found: '%s'" expected_str actual_str + in + assert_equal ~cmp:(List.equal ~equal:Int.equal) ~pp_diff expected found + in + [ (* + === test1 === + File1 and File2 are empty + *) + ("test_parse_directives_with_valid_input_1", [], []) + (* + === test2 === + File1 File2 + a x + b + c + d + *) + ; ("test_parse_directives_with_valid_input_2", o 4 @ n 1, [1]) + (* + === test3 === + File1 File2 + a x + v b + b c + c Z + *) + ; ("test_parse_directives_with_valid_input_3", o 2 @ n 1 @ u 2 @ n 1, [1; 4]) + (* + === test4 === + File1 File2 + a w + b x + c y + z + *) + ; ("test_parse_directives_with_valid_input_4", o 3 @ n 4, [1; 2; 3; 4]) + (* + === test5 === + File1 File2 + a a + b b + c c + d d + e e + w + x + y + *) + ; ("test_parse_directives_with_valid_input_5", u 5 @ n 3, [6; 7; 8]) + (* + === test6 === + File1 File2 + a a + b b + c c + d d + e e + f i + g + h + i + *) + ; ("test_parse_directives_with_valid_input_6", u 5 @ o 3 @ u 1, [6]) + (* + === test7 === + File1 File2 + a + b + c + *) + ; ("test_parse_directives_with_valid_input_7", o 3, [1]) + (* + === test8 === + File1 File2 + a a + b d + c x + d y + *) + ; ("test_parse_directives_with_valid_input_8", u 1 @ o 2 @ u 1 @ n 2, [2; 3; 4]) + (* + === test9 === + File1 File2 + a d + b x + c y + d z + *) + ; ("test_parse_directives_with_valid_input_9", o 3 @ u 1 @ n 3, [1; 2; 3; 4]) + (* + === test10 === + File1 File2 + a a + b x + c d + d y + e z + *) + ; ("test_parse_directives_with_valid_input_10", u 1 @ o 2 @ n 1 @ u 1 @ o 1 @ n 2, [2; 4; 5]) + (* + === test11 === + File1 File2 + a a + b b + c x + d c + e y + *) + ; ("test_parse_directives_with_valid_input_11", u 2 @ n 1 @ u 1 @ o 2 @ n 1, [3; 5]) + ; ( "test_parse_directives_with_valid_input_12" + , o 1 @ n 1 @ u 6 @ o 2 @ n 2 @ u 5 @ o 2 @ n 2 @ u 244 @ o 12 @ u 3 @ o 1 @ n 1 @ u 3 + , [1; 8; 9; 15; 16; 261; 264] ) + (* + === test13 === + File1 File2 + a a + b b + c c + d x + e y + d + e + *) + ; ("test_parse_directives_with_valid_input_13", u 3 @ n 2 @ u 2, [4; 5]) + (* + === test14 === + File1 File2 + a e + b w + c x + d y + e z + *) + ; ("test_parse_directives_with_valid_input_14", o 4 @ u 1 @ n 4, [1; 2; 3; 4; 5]) + (* + === test15 === + File1 File2 + a a + b x + c + d + e + *) + ; ("test_parse_directives_with_valid_input_15", u 1 @ o 4 @ n 1, [2]) + (* + === test16 === + File1 File2 + a x + b e + c + d + e + *) + ; ("test_parse_directives_with_valid_input_16", o 4 @ n 1 @ u 1, [1]) + (* + === test17 === + File1 File2 + a x + v b + b c + c + *) + ; ("test_parse_directives_with_valid_input_17", o 2 @ n 1 @ u 2, [1]) ] + |> List.map ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output ) + + +let test_parse_unix_diff_with_valid_input = + let create_test input expected _ = + let found = FileDiff.parse_unix_diff input in + let pp_diff fmt (expected, actual) = + let expected_str = Format.asprintf "%a" (Pp.seq ~sep:", " Format.pp_print_int) expected in + let actual_str = Format.asprintf "%a" (Pp.seq ~sep:", " Format.pp_print_int) actual in + Format.fprintf fmt "Expected: '%s', found: '%s'" expected_str actual_str + in + assert_equal ~cmp:(List.equal ~equal:Int.equal) ~pp_diff expected found + in + [("test_parse_unix_diff_1", "OONUU", [1]); ("test_parse_unix_diff_2", "UOONUONN", [2; 4; 5])] + |> List.map ~f:(fun (name, test_input, expected_output) -> + name >:: create_test test_input expected_output ) + + +let tests = + "filediff" + >::: test_unixdiff_process_raw_directives_with_valid_input + @ test_unixdiff_process_raw_directives_with_invalid_input @ test_unixdiff_pp + @ test_parse_directives_with_valid_input @ test_parse_unix_diff_with_valid_input diff --git a/infer/src/unit/inferunit.ml b/infer/src/unit/inferunit.ml index b580885e5..95b00d9ae 100644 --- a/infer/src/unit/inferunit.ml +++ b/infer/src/unit/inferunit.ml @@ -35,6 +35,7 @@ let () = ; BoundedCallTreeTests.tests ; DifferentialTests.tests ; DifferentialFiltersTests.tests + ; FileDiffTests.tests ; JavaProfilerSamplesTest.tests ; ProcCfgTests.tests ; LivenessTests.tests