Skip to content

Commit

Permalink
A simple preprocessor for manipulating CN tutorial files.
Browse files Browse the repository at this point in the history
  • Loading branch information
yav committed Dec 10, 2024
1 parent 894567d commit e9288ed
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 0 deletions.
3 changes: 3 additions & 0 deletions preprocessor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
preproc_tut
*.cmi
*.cmo
10 changes: 10 additions & 0 deletions preprocessor/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
preproc_tut: preproc_tut.ml
ocamlc -o $@ $<

.PHONY: clean veryclean

clean:
rm -rf *.cmi *.cmo

veryclean: clean
rm -rf preproc_tut
84 changes: 84 additions & 0 deletions preprocessor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Overview
========

This is a small tool for pre-processing the example files in the tutorial.

We would like to ensure that CN's testing infrastructure works well with the
files in the tutorial, and so it is convenient to add additional annotations
to the files to support:
* mutation testing, to ensure that property based testing is catching
mistakes
* custom unit test entry points, to ensure that executing specificatoins
works as expected.

Both of these could, in principle, be useful to end user of CN, but for
tutorial purposes it is also conveninet to be able to remove these before
showing the files for the users.

This preprocessor is intended to help with this task.
To build it run `make`, and optionally `make clean`. The result
should be an executable called `preproc_tut`.

Run `preproc_tut help` to see a list of available commands.


Notation for Mutation Testing
=============================

The pre-processor is line based. For mutation testing we use a CPP-like
if-block, as illustrated by the following example:
```
#if !MUTATION
Normal
code
path
#elif MutationName
Code for
some
mutation
#elif AnotherMutationName
Some other variant
#endif
```


If we run the pre-processer to eliminate mutation testing the result would
be only:

```
Normal
code
path
```


If we run the pre-processort to generate input for the Etna mutation testing
tool, we'd get:

```
//! //
Normal
code
path
//!! MutationName // //!
Code for
some
mutation
// //!! AnotherMutationName // //!
Some other variant
//
```


Unit Tests
==========

Unit tests are written as CPP conditionals where the condition is
an identifier that starts with `CN_TEST`. For example:

```
#if CN_TEST
Lines only
for test
#endif
```
198 changes: 198 additions & 0 deletions preprocessor/preproc_tut.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@

let drop_prefix prefix str =
if String.starts_with ~prefix str
then
let n = String.length prefix in
let l = String.length str in
Some (String.trim (String.sub str n (l - n)))
else None

(* Should we start a new mutant block *)
let start_mutant_block line =
match drop_prefix "#if" line with
| Some "!MUTATION" -> true
| _ -> false

(* Does this line start a mutant *)
let start_mutant = drop_prefix "#elif"

let start_unit_test line =
match drop_prefix "#if" line with
| Some txt -> drop_prefix "CN_TEST" txt (* XXX: space or underscore separator? *)
| _ -> None

(* Ending for mutant blocks and units tests *)
let end_named_block = String.starts_with ~prefix:"#endif"



(* -------------------------------------------------------------------------- *)

type named_lines = {
start_line: int; (* starting line number *)
name: string;
lines: string list;
}

let finish_named_lines (mu: named_lines) =
{ mu with lines = List.rev mu.lines }

(* Some content and its mutants. *)
type mutant_block = {
start_line: int; (* starting line number *)
orig: string list;
variants: named_lines list;
}

type content =
| Mutants of mutant_block
| UnitTest of named_lines
| Line of string

type state =
| TopLevel
| InMutantOrig of int
| InMutant of int
| InUnitTest of (int * string)

(* -------------------------------------------------------------------------- *)

type mode =
| NoTesting (* Remove testing related lines *)
| MutationTesting (* Translate to Etna notation *)
| CollectUnitTest (* Print only names of unit tests *)
| ExecuteUnitTest of string (* Print only this specific unit test *)

let rec process_input mode start_line state =
let mb_line = try Some (read_line()) with End_of_file -> None in
match mb_line with
| None ->
begin
let mk_error no msg = failwith (Printf.sprintf "%d: %s" no msg) in
match state with
| TopLevel -> ()
| InMutantOrig n -> mk_error n "Untermianted mutant block"
| InMutant n -> mk_error n "Unterminated mutant block"
| InUnitTest (n,_) -> mk_error n "Unterminated unit test"
end
| Some line ->
let new_state =
match state with

(* start a mutation test *)
| TopLevel when start_mutant_block line ->
begin match mode with
| MutationTesting -> print_endline "//! //"
| _ -> ()
end;
InMutantOrig start_line (* next state *)

| TopLevel ->
begin match start_unit_test line with

(* start a unit test *)
| Some name ->
begin match mode with
| CollectUnitTest -> Printf.printf "CN_TEST%s\n" name
| _ -> ()
end;
InUnitTest (start_line, name) (* next state *)

(* ordinary top level line *)
| None ->
begin match mode with
| CollectUnitTest -> ()
| _ -> print_endline line
end;
TopLevel (* next state *)
end

| InMutantOrig ln ->
begin match start_mutant line with

(* Start a mutant *)
| Some name ->
begin match mode with
| MutationTesting -> Printf.printf "//!! %s // //!\n" name
| _ -> ()
end;
InMutant ln (* next state *)

(* Original part of a mutation block *)
| None ->
begin match mode with
| CollectUnitTest -> ()
| _ -> print_endline line
end;
InMutantOrig ln (* next state *)
end

(* End mutant block *)
| InMutant ln when end_named_block line ->
begin match mode with
| MutationTesting -> print_endline "//"
| _ -> ()
end;
TopLevel (* next state *)

| InMutant ln ->

begin match start_mutant line with
(* Next mutatant *)
| Some name ->
begin match mode with
| MutationTesting -> Printf.printf "// //!! %s // //!\n" name
| _ -> ()
end;
InMutant ln (* next state *)

(* Line in a mutant *)
| None ->
begin match mode with
| MutationTesting -> print_endline line
| _ -> ()
end;
InMutant ln (* next state *)
end

(* End unit test *)
| InUnitTest (ln,name) when end_named_block line ->
TopLevel (* next state *)

(* Line in a unit test *)
| InUnitTest (ln,name) ->
begin match mode with
| ExecuteUnitTest t when String.equal name t -> print_endline line
| _ -> ()
end;
InUnitTest (ln,name) (* next state *)

in process_input mode (start_line + 1) new_state

let show_usage name =
Printf.eprintf "USAGE: %s COMMAND\n" name;
Printf.eprintf "\
Process lines from `stdin` to `stdout` depending on COMMAND\n\
Valid comamnds are:\n\
* no_test Remove all annotation related to testing.\n\
* etna Emit mutation tests in CN Etna notation.\n\
* list_unit List names of available unit tests.\n\
* UNIT_TEST Emit content relevant for UNIT_TEST.\n\
"



let parse_mode str =
match str with
| "no_test" -> Some NoTesting
| "etna" -> Some MutationTesting
| "list_unit" -> Some CollectUnitTest
| _ -> match drop_prefix "CN_TEST" str with
| Some t -> Some (ExecuteUnitTest t)
| None -> None

let () =
if not (Int.equal (Array.length Sys.argv) 2) then show_usage Sys.argv.(0);
match parse_mode Sys.argv.(1) with
| Some mode -> process_input mode 1 TopLevel
| None -> show_usage Sys.argv.(0)

0 comments on commit e9288ed

Please sign in to comment.