differential-coverage 1.0.2__tar.gz → 1.0.4__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 (24) hide show
  1. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/PKG-INFO +1 -1
  2. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/pyproject.toml +1 -1
  3. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/fs.py +5 -1
  4. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/output.py +70 -27
  5. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/PKG-INFO +1 -1
  6. differential_coverage-1.0.4/tests/test_output.py +99 -0
  7. differential_coverage-1.0.2/tests/test_output.py +0 -64
  8. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/README.md +0 -0
  9. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/setup.cfg +0 -0
  10. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/__init__.py +0 -0
  11. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/api.py +0 -0
  12. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/approach_data.py +0 -0
  13. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/cli.py +0 -0
  14. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage/types.py +0 -0
  15. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/SOURCES.txt +0 -0
  16. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/dependency_links.txt +0 -0
  17. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/entry_points.txt +0 -0
  18. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/requires.txt +0 -0
  19. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/src/differential_coverage.egg-info/top_level.txt +0 -0
  20. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/tests/test_api.py +0 -0
  21. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/tests/test_approach_data.py +0 -0
  22. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/tests/test_cli.py +0 -0
  23. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/tests/test_docs.py +0 -0
  24. {differential_coverage-1.0.2 → differential_coverage-1.0.4}/tests/test_reducers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: differential_coverage
3
- Version: 1.0.2
3
+ Version: 1.0.4
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.0.2"
7
+ version = "1.0.4"
8
8
  description = "Compute differential coverage, a better way of comparing testing tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,7 +37,11 @@ def read_campaign_dir(
37
37
  campaigns: dict[str, dict[str, set[str]]] = {}
38
38
  for approach_dir in path.iterdir():
39
39
  if approach_dir.is_dir():
40
- campaigns[approach_dir.name] = read_approach_dir(approach_dir)
40
+ approach_data = read_approach_dir(approach_dir)
41
+ if len(approach_data) == 0:
42
+ print(f"Warning: No coverage data in {approach_dir}. Skipping.")
43
+ continue
44
+ campaigns[approach_dir.name] = approach_data
41
45
  else:
42
46
  raise ValueError(f"Invalid file: {approach_dir}")
43
47
  return campaigns
@@ -3,8 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import csv
6
- import sys
7
- from collections.abc import Mapping, Sequence
6
+ from collections.abc import Callable, Mapping, Sequence
8
7
  from typing import Literal, Optional
9
8
 
10
9
 
@@ -13,26 +12,46 @@ from differential_coverage.types import ApproachId
13
12
  OutputFormat = Literal["stdout", "csv", "latex"]
14
13
 
15
14
 
15
+ class _PrinterIO:
16
+ """Minimal file-like wrapper that forwards writes to a printer function."""
17
+
18
+ def __init__(self, printer: Callable[[str], None]) -> None:
19
+ self._printer = printer
20
+
21
+ def write(self, s: str) -> int: # pragma: no cover - trivial adapter
22
+ # csv.writer may call write multiple times per row; strip trailing newlines
23
+ # and avoid printing empty chunks.
24
+ if not s:
25
+ return 0
26
+ if s.endswith("\n"):
27
+ s = s[:-1]
28
+ if s:
29
+ self._printer(s)
30
+ return len(s)
31
+
32
+
16
33
  def print_scores(
17
34
  scores: Mapping[ApproachId, float],
18
35
  *,
19
36
  output: OutputFormat = "stdout",
20
37
  colormap: str = "viridis",
21
38
  latex_enable_color: bool = False,
39
+ printer: Callable[[str], None] = print,
22
40
  ) -> None:
23
41
  """Print one score per approach, in the requested output format."""
24
42
  name_sorted = sorted(scores.items(), key=lambda x: x[0])
25
43
  performance_sorted = sorted(name_sorted, key=lambda x: x[1], reverse=True)
26
44
 
27
45
  if output == "stdout":
28
- _print_scores_plain(performance_sorted)
46
+ _print_scores_plain(performance_sorted, printer=printer)
29
47
  elif output == "csv":
30
- _print_scores_csv(performance_sorted)
48
+ _print_scores_csv(performance_sorted, printer=printer)
31
49
  elif output == "latex":
32
50
  _print_scores_latex(
33
51
  performance_sorted,
34
52
  enable_color=latex_enable_color,
35
53
  colormap=colormap,
54
+ printer=printer,
36
55
  )
37
56
  else:
38
57
  raise ValueError(f"Invalid output format: {output}")
@@ -40,15 +59,19 @@ def print_scores(
40
59
 
41
60
  def _print_scores_plain(
42
61
  performance_sorted: Sequence[tuple[ApproachId, float]],
62
+ *,
63
+ printer: Callable[[str], None],
43
64
  ) -> None:
44
65
  for approach, score in performance_sorted:
45
- print(f"{approach}: {score:.2f}")
66
+ printer(f"{approach}: {score:.2f}")
46
67
 
47
68
 
48
69
  def _print_scores_csv(
49
70
  performance_sorted: Sequence[tuple[ApproachId, float]],
71
+ *,
72
+ printer: Callable[[str], None],
50
73
  ) -> None:
51
- writer = csv.writer(sys.stdout)
74
+ writer = csv.writer(_PrinterIO(printer))
52
75
  writer.writerow(("approach", "score"))
53
76
  for approach, score in performance_sorted:
54
77
  writer.writerow((approach, f"{score:.2f}"))
@@ -73,14 +96,15 @@ def _print_scores_latex(
73
96
  *,
74
97
  enable_color: bool = False,
75
98
  colormap: str = "viridis",
99
+ printer: Callable[[str], None],
76
100
  ) -> None:
77
101
  """LaTeX table (optionally with score cells colored by value)."""
78
- print(r"\begin{tabular}{lr}")
79
- print(r"approach & score \\")
102
+ printer(r"\begin{tabular}{lr}")
103
+ printer(r"approach & score \\")
80
104
  if not performance_sorted:
81
- print(r"\end{tabular}")
105
+ printer(r"\end{tabular}")
82
106
  return
83
- print(r"\hline")
107
+ printer(r"\hline")
84
108
  min_v, max_v = (0.0, 0.0)
85
109
  if enable_color:
86
110
  values = [s for _, s in performance_sorted]
@@ -89,10 +113,10 @@ def _print_scores_latex(
89
113
  if enable_color:
90
114
  n = _norm_value(score, min_v, max_v)
91
115
  hex_color = _colormap_light_hex(n, colormap=colormap)
92
- print(rf"{approach} & \cellcolor[HTML]{{{hex_color}}}{{{score:.2f}}} \\")
116
+ printer(rf"{approach} & \cellcolor[HTML]{{{hex_color}}}{{{score:.2f}}} \\")
93
117
  else:
94
- print(f"{approach} & {score:.2f} \\\\")
95
- print(r"\end{tabular}")
118
+ printer(f"{approach} & {score:.2f} \\\\")
119
+ printer(r"\end{tabular}")
96
120
 
97
121
 
98
122
  def print_relcov_corpus_table(
@@ -103,16 +127,25 @@ def print_relcov_corpus_table(
103
127
  colormap: str = "viridis",
104
128
  latex_rotate_headers: Optional[float] = None,
105
129
  latex_enable_color: bool = False,
130
+ printer: Callable[[str], None] = print,
106
131
  ) -> None:
107
132
  """Print a table of relcov performance: rows = approaches, columns = corpus approaches."""
108
133
  if not corpus_approaches:
109
- print("No corpus approaches (single-trial subdirs) found.")
134
+ printer("No corpus approaches (single-trial subdirs) found.")
110
135
  return
111
136
 
112
137
  if output == "stdout":
113
- _print_relcov_corpus_table_plain(corpus_approaches, table)
138
+ _print_relcov_corpus_table_plain(
139
+ corpus_approaches,
140
+ table,
141
+ printer=printer,
142
+ )
114
143
  elif output == "csv":
115
- _print_relcov_corpus_table_csv(corpus_approaches, table)
144
+ _print_relcov_corpus_table_csv(
145
+ corpus_approaches,
146
+ table,
147
+ printer=printer,
148
+ )
116
149
  elif output == "latex":
117
150
  _print_relcov_corpus_table_latex(
118
151
  corpus_approaches,
@@ -120,6 +153,7 @@ def print_relcov_corpus_table(
120
153
  rotate_headers=latex_rotate_headers,
121
154
  enable_color=latex_enable_color,
122
155
  colormap=colormap,
156
+ printer=printer,
123
157
  )
124
158
  else:
125
159
  raise ValueError(f"Invalid output format: {output}")
@@ -128,6 +162,8 @@ def print_relcov_corpus_table(
128
162
  def _print_relcov_corpus_table_plain(
129
163
  corpus_approaches: Sequence[ApproachId],
130
164
  table: Mapping[ApproachId, Mapping[ApproachId, float]],
165
+ *,
166
+ printer: Callable[[str], None],
131
167
  ) -> None:
132
168
  row_labels = sorted(table.keys())
133
169
  col_labels = sorted(corpus_approaches)
@@ -139,7 +175,7 @@ def _print_relcov_corpus_table_plain(
139
175
  header = "approach".ljust(col_width)
140
176
  for c in col_labels:
141
177
  header += str(c).rjust(num_width)
142
- print(header)
178
+ printer(header)
143
179
  for row in row_labels:
144
180
  line = str(row).ljust(col_width)
145
181
  for c in col_labels:
@@ -148,16 +184,18 @@ def _print_relcov_corpus_table_plain(
148
184
  line += f"{val:>{num_width}.5f}"
149
185
  else:
150
186
  line += " " * num_width
151
- print(line)
187
+ printer(line)
152
188
 
153
189
 
154
190
  def _print_relcov_corpus_table_csv(
155
191
  corpus_approaches: Sequence[ApproachId],
156
192
  table: Mapping[ApproachId, Mapping[ApproachId, float]],
193
+ *,
194
+ printer: Callable[[str], None],
157
195
  ) -> None:
158
196
  row_labels = sorted(table.keys())
159
197
  col_labels = sorted(corpus_approaches)
160
- writer = csv.writer(sys.stdout)
198
+ writer = csv.writer(_PrinterIO(printer))
161
199
  writer.writerow(["approach"] + list(col_labels))
162
200
  for row in row_labels:
163
201
  cells: list[str] = [str(row)]
@@ -167,7 +205,11 @@ def _print_relcov_corpus_table_csv(
167
205
  writer.writerow(cells)
168
206
 
169
207
 
170
- def _latex_print_rotcol_command(*, angle: Optional[float] = None) -> None:
208
+ def _latex_print_rotcol_command(
209
+ *,
210
+ angle: Optional[float] = None,
211
+ printer: Callable[[str], None],
212
+ ) -> None:
171
213
  """Emit the \\rotcol LaTeX command definition.
172
214
  Requires \\usepackage{graphicx} and \\usepackage{calc}.
173
215
  Rotated text is raised so it stays in the header row; row height is \\widthof{#1}.
@@ -177,7 +219,7 @@ def _latex_print_rotcol_command(*, angle: Optional[float] = None) -> None:
177
219
 
178
220
  # Raise rotated text by half its width so it doesn't extend below the baseline
179
221
  # into the first data row; then reserve full width as row height.
180
- print(
222
+ printer(
181
223
  r"""\newcolumntype{R}[2]{%
182
224
  >{\adjustbox{angle=#1,lap=\width-(#2)}\bgroup}%
183
225
  l%
@@ -213,6 +255,7 @@ def _print_relcov_corpus_table_latex(
213
255
  rotate_headers: Optional[float] = None,
214
256
  enable_color: bool = False,
215
257
  colormap: str = "viridis",
258
+ printer: Callable[[str], None],
216
259
  ) -> None:
217
260
  try:
218
261
  from pylatex.utils import escape_latex # type: ignore[import-untyped] # does not provide types
@@ -229,13 +272,13 @@ def _print_relcov_corpus_table_latex(
229
272
 
230
273
  num_cols = 1 + len(col_labels)
231
274
  align_spec = "l" + "r" * (num_cols - 1)
232
- _latex_print_rotcol_command(angle=rotate_headers)
233
- print(r"\begin{tabular}{" + align_spec + r"}")
275
+ _latex_print_rotcol_command(angle=rotate_headers, printer=printer)
276
+ printer(r"\begin{tabular}{" + align_spec + r"}")
234
277
  header_cells = [""] + [
235
278
  _latex_rotcol(escape_latex(str(c)), angle=rotate_headers) for c in col_labels
236
279
  ]
237
- print(" & ".join(header_cells) + r" \\")
238
- print(r"\hline")
280
+ printer("\t" + " & ".join(header_cells) + r" \\")
281
+ printer(r"\hline")
239
282
  for row in row_labels:
240
283
  cells: list[str] = [escape_latex(str(row))]
241
284
  for c in col_labels:
@@ -248,8 +291,8 @@ def _print_relcov_corpus_table_latex(
248
291
  cells.append(rf"\cellcolor[HTML]{{{hex_color}}}{{{val:.3f}}}")
249
292
  else:
250
293
  cells.append(f"{val:.3f}")
251
- print(" & ".join(cells) + r" \\")
252
- print(r"\end{tabular}")
294
+ printer("\t" + " & ".join(cells) + r" \\")
295
+ printer(r"\end{tabular}")
253
296
 
254
297
 
255
298
  def _colormap_light_hex(t: float, *, colormap: str = "viridis") -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: differential_coverage
3
- Version: 1.0.2
3
+ Version: 1.0.4
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
@@ -0,0 +1,99 @@
1
+ """Unit tests for differential_coverage output helpers."""
2
+
3
+ import csv
4
+ import io
5
+ from typing import Callable
6
+
7
+ from differential_coverage.output import print_relcov_corpus_table, print_scores
8
+
9
+ # Deliberately unsorted order for both corpus (columns) and table (rows)
10
+ _UNSORTED_CORPUS = ["z_approach", "a_approach", "m_approach"]
11
+ _UNSORTED_TABLE = {
12
+ "z_approach": {"z_approach": 1.0, "a_approach": 0.2, "m_approach": 0.3},
13
+ "a_approach": {"z_approach": 0.1, "a_approach": 1.0, "m_approach": 0.4},
14
+ "m_approach": {"z_approach": 0.5, "a_approach": 0.6, "m_approach": 1.0},
15
+ }
16
+ _EXPECTED_ROWS = ["a_approach", "m_approach", "z_approach"]
17
+ _EXPECTED_COLS = ["a_approach", "m_approach", "z_approach"]
18
+
19
+
20
+ def _capture_printer() -> tuple[list[str], Callable[[str], None]]:
21
+ lines: list[str] = []
22
+
23
+ def printer(s: str) -> None:
24
+ lines.append(s)
25
+
26
+ return lines, printer
27
+
28
+
29
+ def test_relcov_table_sorted_stdout() -> None:
30
+ """Relcov table stdout output has rows and columns sorted alphabetically."""
31
+ captured, printer = _capture_printer()
32
+ print_relcov_corpus_table(
33
+ _UNSORTED_CORPUS,
34
+ _UNSORTED_TABLE,
35
+ output="stdout",
36
+ printer=printer,
37
+ )
38
+
39
+ lines = [s.strip() for s in "\n".join(captured).strip().splitlines()]
40
+ header = lines[0]
41
+ assert header.startswith("approach")
42
+ row_labels = [line.split()[0] for line in lines[1:]]
43
+ assert row_labels == _EXPECTED_ROWS
44
+ pos = 0
45
+ for col in _EXPECTED_COLS:
46
+ idx = header.find(col, pos)
47
+ assert idx >= 0, f"column {col!r} not found in header"
48
+ pos = idx + len(col)
49
+
50
+
51
+ def test_relcov_table_sorted_csv() -> None:
52
+ """Relcov table CSV output has rows and columns sorted alphabetically."""
53
+ captured, printer = _capture_printer()
54
+ print_relcov_corpus_table(
55
+ _UNSORTED_CORPUS,
56
+ _UNSORTED_TABLE,
57
+ output="csv",
58
+ printer=printer,
59
+ )
60
+
61
+ csv_text = "\n".join(captured)
62
+ reader = csv.reader(io.StringIO(csv_text))
63
+ rows = list(reader)
64
+ assert rows[0][0] == "approach"
65
+ assert rows[0][1:] == _EXPECTED_COLS
66
+ assert [r[0] for r in rows[1:]] == _EXPECTED_ROWS
67
+
68
+
69
+ def test_print_scores_uses_printer_stdout() -> None:
70
+ """print_scores writes human-readable scores via the provided printer."""
71
+ scores = {"b": 0.1, "a": 1.0}
72
+ captured, printer = _capture_printer()
73
+
74
+ print_scores(scores, output="stdout", printer=printer)
75
+
76
+ # Scores should be sorted by value descending, then by name
77
+ joined = "\n".join(captured)
78
+ assert "a: 1.00" in joined
79
+ assert "b: 0.10" in joined
80
+ # Ensure our custom printer was used (no direct writes to sys.stdout here)
81
+ assert joined.count("\n") >= 0
82
+
83
+
84
+ def test_print_scores_uses_printer_csv() -> None:
85
+ """print_scores CSV output is routed through the provided printer."""
86
+ scores = {"b": 0.1, "a": 1.0}
87
+ captured, printer = _capture_printer()
88
+
89
+ print_scores(scores, output="csv", printer=printer)
90
+
91
+ csv_text = "\n".join(captured)
92
+ reader = csv.reader(io.StringIO(csv_text))
93
+ rows = list(reader)
94
+
95
+ # Header + two data rows
96
+ assert rows[0] == ["approach", "score"]
97
+ # Order should be by score descending
98
+ assert rows[1][0] == "a"
99
+ assert rows[2][0] == "b"
@@ -1,64 +0,0 @@
1
- """Unit tests for differential_coverage output helpers."""
2
-
3
- import csv
4
- import io
5
- import sys
6
-
7
- from differential_coverage.output import print_relcov_corpus_table
8
-
9
- # Deliberately unsorted order for both corpus (columns) and table (rows)
10
- _UNSORTED_CORPUS = ["z_approach", "a_approach", "m_approach"]
11
- _UNSORTED_TABLE = {
12
- "z_approach": {"z_approach": 1.0, "a_approach": 0.2, "m_approach": 0.3},
13
- "a_approach": {"z_approach": 0.1, "a_approach": 1.0, "m_approach": 0.4},
14
- "m_approach": {"z_approach": 0.5, "a_approach": 0.6, "m_approach": 1.0},
15
- }
16
- _EXPECTED_ROWS = ["a_approach", "m_approach", "z_approach"]
17
- _EXPECTED_COLS = ["a_approach", "m_approach", "z_approach"]
18
-
19
-
20
- def test_relcov_table_sorted_stdout() -> None:
21
- """Relcov table stdout output has rows and columns sorted alphabetically."""
22
- out = io.StringIO()
23
- old_stdout = sys.stdout
24
- try:
25
- sys.stdout = out
26
- print_relcov_corpus_table(
27
- _UNSORTED_CORPUS,
28
- _UNSORTED_TABLE,
29
- output="stdout",
30
- )
31
- finally:
32
- sys.stdout = old_stdout
33
-
34
- lines = [s.strip() for s in out.getvalue().strip().splitlines()]
35
- header = lines[0]
36
- assert header.startswith("approach")
37
- row_labels = [line.split()[0] for line in lines[1:]]
38
- assert row_labels == _EXPECTED_ROWS
39
- pos = 0
40
- for col in _EXPECTED_COLS:
41
- idx = header.find(col, pos)
42
- assert idx >= 0, f"column {col!r} not found in header"
43
- pos = idx + len(col)
44
-
45
-
46
- def test_relcov_table_sorted_csv() -> None:
47
- """Relcov table CSV output has rows and columns sorted alphabetically."""
48
- out = io.StringIO()
49
- old_stdout = sys.stdout
50
- try:
51
- sys.stdout = out
52
- print_relcov_corpus_table(
53
- _UNSORTED_CORPUS,
54
- _UNSORTED_TABLE,
55
- output="csv",
56
- )
57
- finally:
58
- sys.stdout = old_stdout
59
-
60
- reader = csv.reader(io.StringIO(out.getvalue()))
61
- rows = list(reader)
62
- assert rows[0][0] == "approach"
63
- assert rows[0][1:] == _EXPECTED_COLS
64
- assert [r[0] for r in rows[1:]] == _EXPECTED_ROWS