Skip to content

Commit 8cfccd0

Browse files
committed
feat: add not supported status code
In case of 69 exit code from an app, the test should be reported as not supported and be considered as non-error status.
1 parent 80fc8e6 commit 8cfccd0

File tree

5 files changed

+91
-17
lines changed

5 files changed

+91
-17
lines changed

fluster/fluster.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def to_test_suite_context(
106106
TestVectorResult.FAIL: "❌",
107107
TestVectorResult.TIMEOUT: "⌛",
108108
TestVectorResult.ERROR: "☠",
109+
TestVectorResult.NOT_SUPPORTED: "○",
109110
}
110111

111112
TEXT_RESULT = {
@@ -114,6 +115,7 @@ def to_test_suite_context(
114115
TestVectorResult.FAIL: "KO",
115116
TestVectorResult.TIMEOUT: "TO",
116117
TestVectorResult.ERROR: "ER",
118+
TestVectorResult.NOT_SUPPORTED: "NS",
117119
}
118120

119121

@@ -376,8 +378,8 @@ def _parse_suite_results(
376378

377379
for vector in suite_decoder_res[1].test_vectors.values():
378380
jcase = junitp.TestCase(vector.name)
379-
if vector.test_result == TestVectorResult.NOT_RUN:
380-
jcase.result = [junitp.Skipped()]
381+
if vector.test_result in [TestVectorResult.NOT_RUN, TestVectorResult.NOT_SUPPORTED]:
382+
jcase.result = [junitp.Skipped(message=vector.test_result.value)]
381383
elif vector.test_result not in [
382384
TestVectorResult.SUCCESS,
383385
TestVectorResult.REFERENCE,
@@ -417,6 +419,7 @@ def _generate_csv_summary(ctx: Context, results: Dict[str, List[Tuple[Decoder, T
417419
TestVectorResult.ERROR: "Error",
418420
TestVectorResult.FAIL: "Fail",
419421
TestVectorResult.NOT_RUN: "Not run",
422+
TestVectorResult.NOT_SUPPORTED: "Not supported",
420423
}
421424
content: Dict[Any, Any] = defaultdict(lambda: defaultdict(dict))
422425
max_vectors = 0
@@ -463,6 +466,17 @@ def _global_stats(
463466
output += "\n|TOTAL|"
464467
for test_suite in test_suites:
465468
output += f"{test_suite.test_vectors_success}/{len(test_suite.test_vectors)}|"
469+
output += "\n|NOT SUPPORTED|"
470+
for test_suite in test_suites:
471+
output += f"{test_suite.test_vectors_not_supported}/{len(test_suite.test_vectors)}|"
472+
output += "\n|FAIL/ERROR|"
473+
for test_suite in test_suites:
474+
failed = (
475+
len(test_suite.test_vectors)
476+
- test_suite.test_vectors_success
477+
- test_suite.test_vectors_not_supported
478+
)
479+
output += f"{failed}/{len(test_suite.test_vectors)}|"
466480
output += "\n|TOTAL TIME|"
467481
for test_suite in test_suites:
468482
# Substract from the total time that took running a test suite on a decoder
@@ -541,14 +555,15 @@ def _generate_global_summary(results: Dict[str, List[Tuple[Decoder, TestSuite]]]
541555
all_decoders.append(decoder)
542556
decoder_names.add(decoder.name)
543557

544-
decoder_totals = {dec.name: {"success": 0, "total": 0} for dec in all_decoders}
558+
decoder_totals = {dec.name: {"success": 0, "total": 0, "not_supported": 0} for dec in all_decoders}
545559
decoder_times = {dec.name: 0.0 for dec in all_decoders}
546560
global_profile_stats: Dict[str, Dict[str, Dict[str, int]]] = {dec.name: {} for dec in all_decoders}
547561

548562
for test_suite_results in results.values():
549563
for decoder, test_suite in test_suite_results:
550564
totals = decoder_totals[decoder.name]
551565
totals["success"] += test_suite.test_vectors_success
566+
totals["not_supported"] += test_suite.test_vectors_not_supported
552567
totals["total"] += len(test_suite.test_vectors)
553568

554569
timeouts = (
@@ -578,6 +593,16 @@ def _generate_global_summary(results: Dict[str, List[Tuple[Decoder, TestSuite]]]
578593
output += "\n|TOTAL|" + "".join(
579594
f"{decoder_totals[dec.name]['success']}/{decoder_totals[dec.name]['total']}|" for dec in all_decoders
580595
)
596+
output += "\n|NOT SUPPORTED|" + "".join(
597+
f"{decoder_totals[dec.name]['not_supported']}/{decoder_totals[dec.name]['total']}|"
598+
for dec in all_decoders
599+
)
600+
fail_error_parts = []
601+
for dec in all_decoders:
602+
totals = decoder_totals[dec.name]
603+
failed = totals["total"] - totals["success"] - totals["not_supported"]
604+
fail_error_parts.append(f"{failed}/{totals['total']}|")
605+
output += "\n|FAIL/ERROR|" + "".join(fail_error_parts)
581606
output += "\n|TOTAL TIME|" + "".join(f"{decoder_times[dec.name]:.3f}s|" for dec in all_decoders)
582607

583608
all_profiles: Set[str] = set()

fluster/test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from fluster.decoder import Decoder
1818
from fluster.test_vector import TestVector, TestVectorResult
19-
from fluster.utils import compare_byte_wise_files, normalize_path
19+
from fluster.utils import NotSupportedError, compare_byte_wise_files, normalize_path
2020

2121

2222
class Test(unittest.TestCase):
@@ -110,6 +110,13 @@ def _test(self) -> None:
110110
try:
111111
result = self._execute_decode()
112112
self.test_vector_result.test_time = perf_counter() - start
113+
except NotSupportedError as ex:
114+
# Exit code 69: media not supported by decoder
115+
self.test_vector_result.test_result = TestVectorResult.NOT_SUPPORTED
116+
self.test_vector_result.test_time = perf_counter() - start
117+
if self.verbose:
118+
print(f" {self.test_vector.name}: {ex.message}")
119+
return
113120
except TimeoutExpired:
114121
self.test_vector_result.test_result = TestVectorResult.TIMEOUT
115122
self.test_vector_result.test_time = perf_counter() - start

fluster/test_suite.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def __init__(
157157
self.filename = filename
158158
self.resources_dir = resources_dir
159159
self.test_vectors_success = 0
160+
self.test_vectors_not_supported = 0
160161
self.time_taken = 0.0
161162

162163
def clone(self) -> "TestSuite":
@@ -174,6 +175,10 @@ def from_json_file(cls: Type["TestSuite"], filename: str, resources_dir: str) ->
174175
data["codec"] = Codec(data["codec"])
175176
if "test_method" in data:
176177
data["test_method"] = TestMethod(data["test_method"])
178+
# Remove runtime-only fields that might be present in old JSON files
179+
data.pop("test_vectors_success", None)
180+
data.pop("test_vectors_not_supported", None)
181+
data.pop("time_taken", None)
177182
return cls(filename, resources_dir, **data)
178183

179184
def to_json_file(self, filename: str) -> None:
@@ -183,6 +188,7 @@ def to_json_file(self, filename: str) -> None:
183188
data.pop("resources_dir")
184189
data.pop("filename")
185190
data.pop("test_vectors_success")
191+
data.pop("test_vectors_not_supported")
186192
data.pop("time_taken")
187193
if self.failing_test_vectors is None:
188194
data.pop("failing_test_vectors")
@@ -469,8 +475,12 @@ def _callback(test_result: TestVector) -> None:
469475
self.time_taken = perf_counter() - start
470476
print("\n")
471477
self.test_vectors_success = 0
478+
self.test_vectors_not_supported = 0
472479
for test_vector_res in test_vector_results:
473-
if test_vector_res.errors:
480+
# Check for NOT_SUPPORTED first
481+
if test_vector_res.test_result == TestVectorResult.NOT_SUPPORTED:
482+
self.test_vectors_not_supported += 1
483+
elif test_vector_res.errors:
474484
if self.negative_test:
475485
self.test_vectors_success += 1
476486
else:
@@ -486,10 +496,13 @@ def _callback(test_result: TestVector) -> None:
486496
# Collect the test vector results and failures since they come
487497
# from a different process
488498
self.test_vectors[test_vector_res.name] = test_vector_res
489-
print(
490-
f"Ran {self.test_vectors_success}/{len(tests)} tests successfully \
491-
in {self.time_taken:.3f} secs"
492-
)
499+
500+
# Build status message
501+
status_parts = [f"{self.test_vectors_success}/{len(tests)} tests successfully"]
502+
if self.test_vectors_not_supported > 0:
503+
status_parts.append(f"{self.test_vectors_not_supported} not supported")
504+
status_parts.append(f"in {self.time_taken:.3f} secs")
505+
print(f"Ran {', '.join(status_parts)}")
493506

494507
def run(self, ctx: Context) -> Optional["TestSuite"]:
495508
"""

fluster/test_vector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TestVectorResult(Enum):
3030
TIMEOUT = "Timeout"
3131
ERROR = "Error"
3232
REFERENCE = "Reference run" # used in reference runs to indicate the decoder for this test vector was succesful
33+
NOT_SUPPORTED = "Not Supported" # used to indicate the decoder cannot handle this media
3334

3435

3536
class TestVector:

fluster/utils.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@
3434

3535
TARBALL_EXTS = ("tar.gz", "tgz", "tar.bz2", "tbz2", "tar.xz")
3636

37+
38+
class NotSupportedError(Exception):
39+
"""Exception raised when decoder cannot handle the media (exit code 69).
40+
41+
Exit code 69 (EX_UNAVAILABLE) is from BSD sysexits.h, meaning "service unavailable".
42+
This is widely recognized across Unix-like systems (Linux, macOS, BSD) and indicates
43+
that the decoder service is unavailable for the specific media format/profile.
44+
"""
45+
46+
def __init__(self, message: str = "Media not supported by decoder"):
47+
self.message = message
48+
super().__init__(self.message)
49+
50+
3751
download_lock = Lock()
3852

3953

@@ -199,7 +213,14 @@ def run_command(
199213
print(f'\nRunning command "{" ".join(command)}"')
200214
try:
201215
subprocess.run(command, stdout=sout, stderr=serr, check=check, timeout=timeout)
202-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex:
216+
except subprocess.CalledProcessError as ex:
217+
# Developer experience improvement (facilitates copy/paste)
218+
ex.cmd = " ".join(ex.cmd)
219+
# Check for exit code 69 (EX_UNAVAILABLE from BSD sysexits.h)
220+
if ex.returncode == 69:
221+
raise NotSupportedError(f"Command returned exit code 69 (not supported): {ex.cmd}") from ex
222+
raise ex
223+
except subprocess.TimeoutExpired as ex:
203224
# Developer experience improvement (facilitates copy/paste)
204225
ex.cmd = " ".join(ex.cmd)
205226
raise ex
@@ -224,17 +245,24 @@ def run_command_with_output(
224245
if verbose and output:
225246
print(output)
226247
return output or ""
227-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex:
248+
except subprocess.CalledProcessError as ex:
228249
if verbose and ex.output:
229-
# Workaround inconsistent Python implementation
230-
if isinstance(ex, subprocess.TimeoutExpired):
231-
print(ex.output.decode("utf-8"))
232-
else:
233-
print(ex.output)
250+
print(ex.output)
234251

235-
if isinstance(ex, subprocess.CalledProcessError) and not check:
252+
if not check:
236253
return ex.output or ""
237254

255+
# Developer experience improvement (facilitates copy/paste)
256+
ex.cmd = " ".join(ex.cmd)
257+
# Check for exit code 69 (EX_UNAVAILABLE from BSD sysexits.h)
258+
if ex.returncode == 69:
259+
raise NotSupportedError(f"Command returned exit code 69 (not supported): {ex.cmd}") from ex
260+
raise ex
261+
except subprocess.TimeoutExpired as ex:
262+
if verbose and ex.output:
263+
# Workaround inconsistent Python implementation
264+
print(ex.output.decode("utf-8"))
265+
238266
# Developer experience improvement (facilitates copy/paste)
239267
ex.cmd = " ".join(ex.cmd)
240268
raise ex

0 commit comments

Comments
 (0)