differential-coverage 1.2.0__tar.gz → 1.2.1__tar.gz

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 (31) hide show
  1. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/PKG-INFO +1 -1
  2. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/pyproject.toml +1 -1
  3. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/cli.py +17 -11
  4. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/output.py +36 -11
  5. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/PKG-INFO +1 -1
  6. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_cli.py +12 -0
  7. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_output.py +29 -2
  8. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/README.md +0 -0
  9. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/setup.cfg +0 -0
  10. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/__init__.py +0 -0
  11. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/api.py +0 -0
  12. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/approach_data.py +0 -0
  13. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/fs.py +0 -0
  14. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/readers/__init__.py +0 -0
  15. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/readers/afl_showmap.py +0 -0
  16. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/readers/libfuzzer_merge.py +0 -0
  17. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/readers/llvm_cov.py +0 -0
  18. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/readers/registry.py +0 -0
  19. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage/types.py +0 -0
  20. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/SOURCES.txt +0 -0
  21. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/dependency_links.txt +0 -0
  22. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/entry_points.txt +0 -0
  23. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/requires.txt +0 -0
  24. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/src/differential_coverage.egg-info/top_level.txt +0 -0
  25. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_api.py +0 -0
  26. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_approach_data.py +0 -0
  27. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_docs.py +0 -0
  28. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_libfuzzer_merge.py +0 -0
  29. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_llvm_cov.py +0 -0
  30. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_readers.py +0 -0
  31. {differential_coverage-1.2.0 → differential_coverage-1.2.1}/tests/test_reducers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: differential_coverage
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Compute differential coverage, a better way of comparing testing tools.
5
5
  Author-email: Valentin Huber <contact@valentinhuber.me>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "differential_coverage"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Compute differential coverage, a better way of comparing testing tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -87,8 +87,9 @@ def cmd_relscore(args: argparse.Namespace) -> int:
87
87
  print_scores(
88
88
  scores,
89
89
  output=args.output,
90
- colormap=getattr(args, "colormap", "viridis"),
91
- latex_enable_color=getattr(args, "latex_enable_color", False),
90
+ colormap=args.colormap,
91
+ latex_enable_color=args.latex_enable_color,
92
+ digits=args.digits,
92
93
  )
93
94
  return 0
94
95
 
@@ -97,11 +98,6 @@ def cmd_relcov_performance_over_approach(args: argparse.Namespace) -> int:
97
98
  campaign = _load_campaign(args)
98
99
 
99
100
  dc = DifferentialCoverage(campaign)
100
- output = args.output
101
- colormap = getattr(args, "colormap", "viridis")
102
- latex_rotate_headers = getattr(args, "latex_rotate_headers", None)
103
- latex_enable_color = getattr(args, "latex_enable_color", False)
104
-
105
101
  ref_approaches = list(dc.approaches.keys())
106
102
  table = {
107
103
  f: {ref: dc.approaches[f].relcov(dc.approaches[ref]) for ref in ref_approaches}
@@ -110,10 +106,11 @@ def cmd_relcov_performance_over_approach(args: argparse.Namespace) -> int:
110
106
  print_relcov_corpus_table(
111
107
  ref_approaches,
112
108
  table,
113
- output=output,
114
- colormap=colormap,
115
- latex_rotate_headers=latex_rotate_headers,
116
- latex_enable_color=latex_enable_color,
109
+ output=args.output,
110
+ colormap=args.colormap,
111
+ latex_rotate_headers=args.latex_rotate_headers,
112
+ latex_enable_color=args.latex_enable_color,
113
+ digits=args.digits,
117
114
  )
118
115
  return 0
119
116
 
@@ -192,6 +189,15 @@ def build_parser() -> argparse.ArgumentParser:
192
189
  'latex for LaTeX tabular (requires "latex" optional dependencies)'
193
190
  ),
194
191
  )
192
+ parser.add_argument(
193
+ "--digits",
194
+ type=int,
195
+ default=3,
196
+ metavar="N",
197
+ help=(
198
+ "Number of digits after the decimal point for numeric results (default: 3)."
199
+ ),
200
+ )
195
201
  parser.add_argument(
196
202
  "--latex-rotate-headers",
197
203
  type=float,
@@ -30,12 +30,22 @@ class _PrinterIO:
30
30
  return len(s)
31
31
 
32
32
 
33
+ # Default decimal places for numeric output.
34
+ _DEFAULT_DIGITS = 3
35
+
36
+
37
+ def _format_float(value: float, digits: int) -> str:
38
+ """Format a float with a fixed number of digits after the decimal point."""
39
+ return f"{value:.{digits}f}"
40
+
41
+
33
42
  def print_scores(
34
43
  scores: Mapping[ApproachId, float],
35
44
  *,
36
45
  output: OutputFormat = "stdout",
37
46
  colormap: str = "viridis",
38
47
  latex_enable_color: bool = False,
48
+ digits: int = _DEFAULT_DIGITS,
39
49
  printer: Callable[[str], None] = print,
40
50
  ) -> None:
41
51
  """Print one score per approach, in the requested output format."""
@@ -43,14 +53,15 @@ def print_scores(
43
53
  performance_sorted = sorted(name_sorted, key=lambda x: x[1], reverse=True)
44
54
 
45
55
  if output == "stdout":
46
- _print_scores_plain(performance_sorted, printer=printer)
56
+ _print_scores_plain(performance_sorted, digits=digits, printer=printer)
47
57
  elif output == "csv":
48
- _print_scores_csv(performance_sorted, printer=printer)
58
+ _print_scores_csv(performance_sorted, digits=digits, printer=printer)
49
59
  elif output == "latex":
50
60
  _print_scores_latex(
51
61
  performance_sorted,
52
62
  enable_color=latex_enable_color,
53
63
  colormap=colormap,
64
+ digits=digits,
54
65
  printer=printer,
55
66
  )
56
67
  else:
@@ -60,21 +71,23 @@ def print_scores(
60
71
  def _print_scores_plain(
61
72
  performance_sorted: Sequence[tuple[ApproachId, float]],
62
73
  *,
74
+ digits: int,
63
75
  printer: Callable[[str], None],
64
76
  ) -> None:
65
77
  for approach, score in performance_sorted:
66
- printer(f"{approach}: {score:.2f}")
78
+ printer(f"{approach}: {_format_float(score, digits)}")
67
79
 
68
80
 
69
81
  def _print_scores_csv(
70
82
  performance_sorted: Sequence[tuple[ApproachId, float]],
71
83
  *,
84
+ digits: int,
72
85
  printer: Callable[[str], None],
73
86
  ) -> None:
74
87
  writer = csv.writer(_PrinterIO(printer))
75
88
  writer.writerow(("approach", "score"))
76
89
  for approach, score in performance_sorted:
77
- writer.writerow((approach, f"{score:.2f}"))
90
+ writer.writerow((approach, _format_float(score, digits)))
78
91
 
79
92
 
80
93
  def _norm_minmax(values: Sequence[float]) -> tuple[float, float]:
@@ -96,6 +109,7 @@ def _print_scores_latex(
96
109
  *,
97
110
  enable_color: bool = False,
98
111
  colormap: str = "viridis",
112
+ digits: int,
99
113
  printer: Callable[[str], None],
100
114
  ) -> None:
101
115
  """LaTeX table (optionally with score cells colored by value)."""
@@ -110,12 +124,13 @@ def _print_scores_latex(
110
124
  values = [s for _, s in performance_sorted]
111
125
  min_v, max_v = _norm_minmax(values)
112
126
  for approach, score in performance_sorted:
127
+ formatted = _format_float(score, digits)
113
128
  if enable_color:
114
129
  n = _norm_value(score, min_v, max_v)
115
130
  hex_color = _colormap_light_hex(n, colormap=colormap)
116
- printer(rf"{approach} & \cellcolor[HTML]{{{hex_color}}}{{{score:.2f}}} \\")
131
+ printer(rf"{approach} & \cellcolor[HTML]{{{hex_color}}}{{{formatted}}} \\")
117
132
  else:
118
- printer(f"{approach} & {score:.2f} \\\\")
133
+ printer(f"{approach} & {formatted} \\\\")
119
134
  printer(r"\end{tabular}")
120
135
 
121
136
 
@@ -127,6 +142,7 @@ def print_relcov_corpus_table(
127
142
  colormap: str = "viridis",
128
143
  latex_rotate_headers: Optional[float] = None,
129
144
  latex_enable_color: bool = False,
145
+ digits: int = _DEFAULT_DIGITS,
130
146
  printer: Callable[[str], None] = print,
131
147
  ) -> None:
132
148
  """Print a table of relcov performance: rows = approaches, columns = corpus approaches."""
@@ -138,12 +154,14 @@ def print_relcov_corpus_table(
138
154
  _print_relcov_corpus_table_plain(
139
155
  corpus_approaches,
140
156
  table,
157
+ digits=digits,
141
158
  printer=printer,
142
159
  )
143
160
  elif output == "csv":
144
161
  _print_relcov_corpus_table_csv(
145
162
  corpus_approaches,
146
163
  table,
164
+ digits=digits,
147
165
  printer=printer,
148
166
  )
149
167
  elif output == "latex":
@@ -153,6 +171,7 @@ def print_relcov_corpus_table(
153
171
  rotate_headers=latex_rotate_headers,
154
172
  enable_color=latex_enable_color,
155
173
  colormap=colormap,
174
+ digits=digits,
156
175
  printer=printer,
157
176
  )
158
177
  else:
@@ -163,6 +182,7 @@ def _print_relcov_corpus_table_plain(
163
182
  corpus_approaches: Sequence[ApproachId],
164
183
  table: Mapping[ApproachId, Mapping[ApproachId, float]],
165
184
  *,
185
+ digits: int,
166
186
  printer: Callable[[str], None],
167
187
  ) -> None:
168
188
  row_labels = sorted(table.keys())
@@ -171,7 +191,8 @@ def _print_relcov_corpus_table_plain(
171
191
  (len(str(c)) for c in col_labels + ["approach"]),
172
192
  default=7,
173
193
  )
174
- num_width = 10 # e.g. " 1.00"
194
+ # Width for aligned numeric columns: sign + integer digit + '.' + fraction + padding.
195
+ num_width = max(10, digits + 4)
175
196
  header = "approach".ljust(col_width)
176
197
  for c in col_labels:
177
198
  header += str(c).rjust(num_width)
@@ -181,7 +202,7 @@ def _print_relcov_corpus_table_plain(
181
202
  for c in col_labels:
182
203
  val = table[row].get(c)
183
204
  if val is not None:
184
- line += f"{val:>{num_width}.5f}"
205
+ line += f"{val:>{num_width}.{digits}f}"
185
206
  else:
186
207
  line += " " * num_width
187
208
  printer(line)
@@ -191,6 +212,7 @@ def _print_relcov_corpus_table_csv(
191
212
  corpus_approaches: Sequence[ApproachId],
192
213
  table: Mapping[ApproachId, Mapping[ApproachId, float]],
193
214
  *,
215
+ digits: int,
194
216
  printer: Callable[[str], None],
195
217
  ) -> None:
196
218
  row_labels = sorted(table.keys())
@@ -201,7 +223,7 @@ def _print_relcov_corpus_table_csv(
201
223
  cells: list[str] = [str(row)]
202
224
  for c in col_labels:
203
225
  val = table[row].get(c)
204
- cells.append(f"{val:.3f}" if val is not None else "")
226
+ cells.append(_format_float(val, digits) if val is not None else "")
205
227
  writer.writerow(cells)
206
228
 
207
229
 
@@ -255,6 +277,7 @@ def _print_relcov_corpus_table_latex(
255
277
  rotate_headers: Optional[float] = None,
256
278
  enable_color: bool = False,
257
279
  colormap: str = "viridis",
280
+ digits: int,
258
281
  printer: Callable[[str], None],
259
282
  ) -> None:
260
283
  try:
@@ -288,9 +311,11 @@ def _print_relcov_corpus_table_latex(
288
311
  elif enable_color:
289
312
  n = _norm_value(val, min_v, max_v)
290
313
  hex_color = _colormap_light_hex(n, colormap=colormap)
291
- cells.append(rf"\cellcolor[HTML]{{{hex_color}}}{{{val:.3f}}}")
314
+ cells.append(
315
+ rf"\cellcolor[HTML]{{{hex_color}}}{{{_format_float(val, digits)}}}"
316
+ )
292
317
  else:
293
- cells.append(f"{val:.3f}")
318
+ cells.append(_format_float(val, digits))
294
319
  printer("\t" + " & ".join(cells) + r" \\")
295
320
  printer(r"\end{tabular}")
296
321
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: differential_coverage
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Compute differential coverage, a better way of comparing testing tools.
5
5
  Author-email: Valentin Huber <contact@valentinhuber.me>
6
6
  License-Expression: MIT
@@ -275,3 +275,15 @@ def test_cli_latex_color_no_cell_colors() -> None:
275
275
  assert any(line.startswith(r"\begin{tabular}") for line in lines)
276
276
  # Table should not contain any colored cells
277
277
  assert not any(r"\cellcolor" in line for line in lines)
278
+
279
+
280
+ def test_cli_digits_relscore() -> None:
281
+ """CLI --digits controls decimal places in relscore output."""
282
+ code, out, _ = _run_cli(["--digits", "4", "relscore", str(SAMPLE_SHOWMAP_DIR)])
283
+ assert code == 0
284
+ lines = [s.strip() for s in out.strip().splitlines()]
285
+ # Each score should have exactly 4 digits after the decimal point
286
+ for line in lines:
287
+ score = line.split(":", 1)[1].strip()
288
+ assert "." in score
289
+ assert len(score.split(".", 1)[1]) == 4
@@ -75,8 +75,8 @@ def test_print_scores_uses_printer_stdout() -> None:
75
75
 
76
76
  # Scores should be sorted by value descending, then by name
77
77
  joined = "\n".join(captured)
78
- assert "a: 1.00" in joined
79
- assert "b: 0.10" in joined
78
+ assert "a: 1.000" in joined
79
+ assert "b: 0.100" in joined
80
80
  # Ensure our custom printer was used (no direct writes to sys.stdout here)
81
81
  assert joined.count("\n") >= 0
82
82
 
@@ -97,3 +97,30 @@ def test_print_scores_uses_printer_csv() -> None:
97
97
  # Order should be by score descending
98
98
  assert rows[1][0] == "a"
99
99
  assert rows[2][0] == "b"
100
+
101
+
102
+ def test_print_scores_respects_digits() -> None:
103
+ """print_scores formats scores with the requested number of digits."""
104
+ scores = {"a": 1.0 / 3.0}
105
+ captured, printer = _capture_printer()
106
+
107
+ print_scores(scores, output="stdout", digits=4, printer=printer)
108
+
109
+ assert captured == ["a: 0.3333"]
110
+
111
+
112
+ def test_relcov_table_respects_digits_csv() -> None:
113
+ """Relcov CSV cells use the requested digit precision."""
114
+ captured, printer = _capture_printer()
115
+ print_relcov_corpus_table(
116
+ ["a"],
117
+ {"a": {"a": 1.0 / 3.0}},
118
+ output="csv",
119
+ digits=4,
120
+ printer=printer,
121
+ )
122
+
123
+ csv_text = "\n".join(captured)
124
+ reader = csv.reader(io.StringIO(csv_text))
125
+ rows = list(reader)
126
+ assert rows[1][1] == "0.3333"