966 lines
42 KiB
Python
966 lines
42 KiB
Python
"""Unit tests for xo-loc.
|
|
|
|
xo-loc lives at xo-cmake/bin/xo-loc.in. The .in suffix is for CMake's
|
|
configure_file(), but the file is deliberately kept valid Python -- the only
|
|
substitution is inside a string literal -- so we can import it directly here
|
|
without running a configure step.
|
|
"""
|
|
|
|
import csv as csvmod
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
import xml.etree.ElementTree as ET
|
|
|
|
_HERE = pathlib.Path(__file__).resolve().parent
|
|
_XO_LOC = _HERE.parent / "bin" / "xo-loc.in"
|
|
|
|
|
|
def load_xo_loc():
|
|
"""Import xo-cmake/bin/xo-loc.in as a module named 'xo_loc'."""
|
|
loader = importlib.machinery.SourceFileLoader("xo_loc", str(_XO_LOC))
|
|
spec = importlib.util.spec_from_loader("xo_loc", loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def load_fixture():
|
|
with open(_HERE / "data" / "scc-sample.json") as f:
|
|
return json.load(f)
|
|
|
|
|
|
class TestModuleLoads(unittest.TestCase):
|
|
def test_script_is_valid_python_and_exposes_a_version(self):
|
|
xo_loc = load_xo_loc()
|
|
self.assertIsInstance(xo_loc.XO_LOC_VERSION, str)
|
|
self.assertTrue(xo_loc.XO_LOC_VERSION)
|
|
|
|
|
|
class TestKindOf(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
|
|
def test_maps_languages_to_kinds(self):
|
|
self.assertEqual(self.xo_loc.kind_of("C++", False), "Code")
|
|
self.assertEqual(self.xo_loc.kind_of("C++ Header", False), "Code")
|
|
self.assertEqual(self.xo_loc.kind_of("CMake", False), "Build")
|
|
self.assertEqual(self.xo_loc.kind_of("Nix", False), "Build")
|
|
self.assertEqual(self.xo_loc.kind_of("Markdown", False), "Docs")
|
|
self.assertEqual(self.xo_loc.kind_of("JSON5", False), "Data")
|
|
|
|
def test_unknown_language_falls_back_to_Other(self):
|
|
self.assertEqual(self.xo_loc.kind_of("Brainfuck", False), "Other")
|
|
|
|
def test_generated_overrides_the_language_mapping(self):
|
|
# a generated .hpp is still C++, but its kind is Generated -- this is
|
|
# why generated cannot be derived from a language lookup alone
|
|
self.assertEqual(self.xo_loc.kind_of("C++ Header", True), "Generated")
|
|
self.assertEqual(self.xo_loc.kind_of("Brainfuck", True), "Generated")
|
|
|
|
|
|
class TestNormalize(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.rows = self.xo_loc.normalize(load_fixture())
|
|
|
|
def test_one_row_per_file(self):
|
|
self.assertEqual(len(self.rows), 8)
|
|
|
|
def test_subsystem_is_first_path_component(self):
|
|
by_path = {r.path: r for r in self.rows}
|
|
self.assertEqual(by_path["xo-gc/src/gc/Collector.cpp"].subsystem, "xo-gc")
|
|
self.assertEqual(
|
|
by_path["xo-imgui/include/imgui/imgui.cpp"].subsystem, "xo-imgui")
|
|
|
|
def test_carries_loc_and_complexity(self):
|
|
row = next(r for r in self.rows if r.path.endswith("Collector.cpp"))
|
|
self.assertEqual(row.loc, 400)
|
|
self.assertEqual(row.complexity, 44)
|
|
|
|
def test_generated_flag_survives_into_the_row(self):
|
|
row = next(r for r in self.rows if r.path.endswith("type/Type.hpp"))
|
|
self.assertTrue(row.generated)
|
|
self.assertEqual(row.kind, "Generated")
|
|
self.assertEqual(row.language, "C++ Header")
|
|
|
|
|
|
class TestNormalizeWalkRoot(unittest.TestCase):
|
|
"""REGRESSION: scc echoes its walk root into Location verbatim.
|
|
|
|
find_root() hands run_scc an ABSOLUTE root, so Location comes back
|
|
absolute; the naive first-path-component split then yields '' for every
|
|
file, nothing matches known_subsystems, and the tool reports 'nothing to
|
|
draw after filtering'. Paths must be made relative to the walk root.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
|
|
def raw(self, location):
|
|
return [{"Name": "C++", "Files": [
|
|
{"Location": location, "Code": 400, "Complexity": 44,
|
|
"Generated": False}]}]
|
|
|
|
def test_absolute_root_prefix_is_stripped(self):
|
|
[row] = self.xo_loc.normalize(
|
|
self.raw("/abs/root/xo-gc/src/gc/Collector.cpp"), root="/abs/root")
|
|
self.assertEqual(row.subsystem, "xo-gc")
|
|
self.assertEqual(row.path, "xo-gc/src/gc/Collector.cpp")
|
|
|
|
def test_trailing_slash_on_the_root_is_tolerated(self):
|
|
[row] = self.xo_loc.normalize(
|
|
self.raw("/abs/root/xo-gc/src/gc/Collector.cpp"), root="/abs/root/")
|
|
self.assertEqual(row.subsystem, "xo-gc")
|
|
|
|
def test_dot_relative_walk_root_is_stripped(self):
|
|
[row] = self.xo_loc.normalize(
|
|
self.raw("./xo-gc/src/gc/Collector.cpp"), root=".")
|
|
self.assertEqual(row.subsystem, "xo-gc")
|
|
self.assertEqual(row.path, "xo-gc/src/gc/Collector.cpp")
|
|
|
|
def test_already_relative_paths_are_left_alone(self):
|
|
[row] = self.xo_loc.normalize(self.raw("xo-gc/src/gc/Collector.cpp"))
|
|
self.assertEqual(row.subsystem, "xo-gc")
|
|
self.assertEqual(row.path, "xo-gc/src/gc/Collector.cpp")
|
|
|
|
|
|
ALL_SUBSYSTEMS = {"xo-gc", "xo-imgui", "xo-type", "xo-cmake", "xo-hashable2"}
|
|
|
|
|
|
class TestSelect(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.rows = self.xo_loc.normalize(load_fixture())
|
|
|
|
def sel(self, **kw):
|
|
return self.xo_loc.select(self.rows, ALL_SUBSYSTEMS, **kw)
|
|
|
|
def test_default_view_drops_vendored_and_generated(self):
|
|
groups = self.sel().groups
|
|
# imgui.cpp (11375 LOC) is vendored -> xo-imgui keeps nothing here
|
|
self.assertNotIn("xo-imgui", groups)
|
|
# xo-type's only file is generated -> nothing left
|
|
self.assertNotIn("xo-type", groups)
|
|
self.assertIn("xo-gc", groups)
|
|
|
|
def test_include_vendored_restores_imgui(self):
|
|
groups = self.sel(include_vendored=True).groups
|
|
self.assertEqual(groups["xo-imgui"]["Code"].loc, 11375)
|
|
|
|
def test_include_generated_restores_it_as_its_own_kind(self):
|
|
groups = self.sel(include_generated=True).groups
|
|
self.assertEqual(groups["xo-type"]["Generated"].loc, 120)
|
|
|
|
def test_aggregates_by_subsystem_and_kind(self):
|
|
gc = self.sel().groups["xo-gc"]
|
|
self.assertEqual(gc["Code"].loc, 490) # 400 .cpp + 90 .hpp
|
|
self.assertEqual(gc["Code"].complexity, 49) # 44 + 5
|
|
self.assertEqual(gc["Code"].nfiles, 2)
|
|
self.assertEqual(gc["Build"].loc, 25) # CMakeLists.txt
|
|
self.assertEqual(gc["Other"].loc, 7) # weird.bf
|
|
|
|
def test_keeps_the_language_breakdown_for_the_tooltip(self):
|
|
gc = self.sel().groups["xo-gc"]
|
|
self.assertEqual(gc["Code"].languages, {"C++": 400, "C++ Header": 90})
|
|
|
|
def test_empty_placeholder_is_reported_not_drawn(self):
|
|
selection = self.sel()
|
|
self.assertNotIn("xo-hashable2", selection.groups)
|
|
self.assertTrue(
|
|
any("xo-hashable2" in n for n in selection.notes),
|
|
"an excluded subsystem must be named in the report")
|
|
|
|
def test_min_loc_tests_total_loc_not_code_loc(self):
|
|
# REGRESSION: xo-cmake is 100% Build and has zero Code LOC, but it is a
|
|
# real subsystem. A 'zero code LOC' emptiness test wrongly drops it.
|
|
groups = self.sel().groups
|
|
self.assertIn("xo-cmake", groups)
|
|
self.assertEqual(groups["xo-cmake"]["Build"].loc, 1066)
|
|
|
|
def test_non_xo_paths_are_ignored(self):
|
|
rows = self.rows + [self.xo_loc.Row(
|
|
subsystem="pkgs", path="pkgs/xo-gc.nix", language="Nix",
|
|
kind="Build", generated=False, loc=99, complexity=0)]
|
|
groups = self.xo_loc.select(rows, ALL_SUBSYSTEMS).groups
|
|
self.assertNotIn("pkgs", groups)
|
|
|
|
def test_positional_subsystems_restrict_the_output(self):
|
|
groups = self.sel(subsystems=["xo-gc"]).groups
|
|
self.assertEqual(set(groups), {"xo-gc"})
|
|
|
|
def test_kinds_filter(self):
|
|
groups = self.sel(kinds=["Build"]).groups
|
|
self.assertEqual(set(groups["xo-gc"]), {"Build"})
|
|
|
|
def test_notes_quantify_every_exclusion(self):
|
|
notes = " | ".join(self.sel().notes)
|
|
self.assertIn("11375", notes) # vendored LOC hidden
|
|
self.assertIn("120", notes) # generated LOC hidden
|
|
|
|
def test_kinds_filter_reports_what_it_hid(self):
|
|
# REGRESSION: --kinds is an exclusion like any other. It silently
|
|
# dropped 26,338 LOC on the real tree, against the spec's rule that
|
|
# "the tool always prints what it hid".
|
|
selection = self.sel(kinds=["Build"])
|
|
notes = " | ".join(selection.notes)
|
|
self.assertIn("--kinds", notes)
|
|
# 490 xo-gc Code + 7 xo-gc Other; vendored and generated were already
|
|
# excluded upstream and are reported by their own notes
|
|
self.assertIn("497", notes)
|
|
|
|
def test_empty_note_respects_positional_subsystems(self):
|
|
# asking for xo-gc should not report placeholders the caller never
|
|
# asked about
|
|
notes = " | ".join(self.sel(subsystems=["xo-gc"]).notes)
|
|
self.assertNotIn("xo-hashable2", notes)
|
|
# ...but it is still named when the whole tree is in view
|
|
self.assertIn("xo-hashable2", " | ".join(self.sel().notes))
|
|
|
|
|
|
class TestSubsystemLoc(unittest.TestCase):
|
|
def test_totals_across_kinds(self):
|
|
xo_loc = load_xo_loc()
|
|
rows = xo_loc.normalize(load_fixture())
|
|
groups = xo_loc.select(rows, ALL_SUBSYSTEMS).groups
|
|
totals = xo_loc.subsystem_loc(groups)
|
|
self.assertEqual(totals["xo-gc"], 522) # 490 Code + 25 Build + 7 Other
|
|
|
|
|
|
class TestSquarify(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
|
|
def area(self, rect):
|
|
return rect.w * rect.h
|
|
|
|
def test_empty_input(self):
|
|
self.assertEqual(self.xo_loc.squarify([], self.canvas), [])
|
|
|
|
def test_single_item_fills_the_rect(self):
|
|
[p] = self.xo_loc.squarify([("a", 10)], self.canvas)
|
|
self.assertEqual(p.key, "a")
|
|
self.assertAlmostEqual(p.rect.w, 1200.0)
|
|
self.assertAlmostEqual(p.rect.h, 800.0)
|
|
|
|
def test_tiles_the_rect_exactly(self):
|
|
items = [("s%d" % i, w) for i, w in
|
|
enumerate([12377, 11011, 7184, 6143, 6075, 5519, 109, 130])]
|
|
placed = self.xo_loc.squarify(items, self.canvas)
|
|
self.assertEqual(len(placed), len(items))
|
|
self.assertAlmostEqual(sum(self.area(p.rect) for p in placed),
|
|
self.area(self.canvas), places=3)
|
|
|
|
def test_area_is_proportional_to_weight(self):
|
|
items = [("a", 3), ("b", 1)]
|
|
placed = {p.key: p.rect for p in self.xo_loc.squarify(items, self.canvas)}
|
|
self.assertAlmostEqual(self.area(placed["a"]) / self.area(placed["b"]),
|
|
3.0, places=6)
|
|
|
|
def test_every_tile_stays_inside_the_canvas(self):
|
|
items = [("s%d" % i, i + 1) for i in range(30)]
|
|
for p in self.xo_loc.squarify(items, self.canvas):
|
|
self.assertGreaterEqual(p.rect.x, -1e-9)
|
|
self.assertGreaterEqual(p.rect.y, -1e-9)
|
|
self.assertLessEqual(p.rect.x + p.rect.w, 1200.0 + 1e-9)
|
|
self.assertLessEqual(p.rect.y + p.rect.h, 800.0 + 1e-9)
|
|
|
|
def test_tiles_do_not_overlap(self):
|
|
items = [("s%d" % i, i + 1) for i in range(12)]
|
|
placed = [p.rect for p in self.xo_loc.squarify(items, self.canvas)]
|
|
for i, a in enumerate(placed):
|
|
for b in placed[i + 1:]:
|
|
separated = (a.x + a.w <= b.x + 1e-6 or b.x + b.w <= a.x + 1e-6
|
|
or a.y + a.h <= b.y + 1e-6 or b.y + b.h <= a.y + 1e-6)
|
|
self.assertTrue(separated, "tiles overlap: %r %r" % (a, b))
|
|
|
|
def test_aspect_ratios_are_reasonable(self):
|
|
# the whole point of *squarified* treemaps: no 1000:1 slivers
|
|
items = [("s%d" % i, w) for i, w in
|
|
enumerate([12377, 11011, 7184, 6143, 6075, 5519, 5002, 4451])]
|
|
for p in self.xo_loc.squarify(items, self.canvas):
|
|
ratio = max(p.rect.w / p.rect.h, p.rect.h / p.rect.w)
|
|
self.assertLess(ratio, 8.0, "sliver: %r" % (p.rect,))
|
|
|
|
def test_non_positive_weights_are_skipped(self):
|
|
placed = self.xo_loc.squarify([("a", 10), ("b", 0), ("c", -1)], self.canvas)
|
|
self.assertEqual([p.key for p in placed], ["a"])
|
|
|
|
def test_degenerate_rect_yields_nothing(self):
|
|
flat = self.xo_loc.Rect(0.0, 0.0, 0.0, 800.0)
|
|
self.assertEqual(self.xo_loc.squarify([("a", 10)], flat), [])
|
|
|
|
|
|
# The real default-view distribution. This MUST be the full 62-subsystem table:
|
|
# F depends on N as well as T, so a trimmed subset changes the answer. With all
|
|
# 62 entries F = 173.45 and exactly three subsystems are floored; with a
|
|
# 14-entry subset F drops to 86 and nothing is floored at all.
|
|
REAL_WEIGHTS = {
|
|
"xo-reader2": 12377, "xo-imgui": 11011, "xo-unit": 7184, "xo-ordinaltree": 6143,
|
|
"xo-gc": 6075, "xo-reader": 5519, "xo-arena": 5002, "xo-expression2": 4451,
|
|
"xo-jit": 4387, "xo-alloc": 4359, "xo-alloc2": 4158, "xo-indentlog": 3949,
|
|
"xo-cmake": 3767, "xo-expression": 3268, "xo-indentlog2": 3127, "xo-tokenizer": 3082,
|
|
"xo-interpreter2": 3040, "xo-websock": 2711, "xo-object2": 2563, "xo-ppsink": 2412,
|
|
"xo-kalmanfilter": 2285, "xo-reflect": 2281, "xo-object": 2275, "xo-ratio": 1950,
|
|
"xo-procedure2": 1908, "xo-reactor": 1850, "xo-interpreter": 1796, "xo-facet": 1766,
|
|
"xo-tokenizer2": 1712, "xo-flatstring": 1509, "xo-type": 1377, "xo-stringtable2": 1355,
|
|
"xo-numeric": 1306, "xo-process": 1255, "xo-refcnt": 845, "xo-simulator": 823,
|
|
"xo-distribution": 804, "xo-pykalmanfilter": 801, "xo-printjson": 699,
|
|
"xo-pysimulator": 612, "xo-randomgen": 464, "xo-pyprocess": 458, "xo-pyreactor": 399,
|
|
"xo-timeutil": 381, "xo-callback": 356, "xo-allocutil": 334, "xo-pyjit": 332,
|
|
"xo-subsys": 305, "xo-pyexpression": 283, "xo-testutil": 270, "xo-webutil": 263,
|
|
"xo-pyprintjson": 254, "xo-pyreflect": 244, "xo-reflectutil": 235, "xo-pywebutil": 224,
|
|
"xo-pyunit": 214, "xo-statistics": 199, "xo-pyutil": 196, "xo-pydistribution": 181,
|
|
"xo-pywebsock": 159, "xo-printable2": 130, "xo-symboltable": 109,
|
|
}
|
|
|
|
|
|
class TestApplyMinArea(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.canvas_area = 1200.0 * 800.0
|
|
|
|
def test_no_floor_needed_leaves_weights_untouched(self):
|
|
weights = {"a": 50000, "b": 50000}
|
|
result = self.xo_loc.apply_min_area(weights, self.canvas_area)
|
|
self.assertEqual(result.weights, weights)
|
|
self.assertEqual(result.floored, [])
|
|
self.assertAlmostEqual(result.inflation, 0.0)
|
|
|
|
def test_floors_only_the_small_subsystems(self):
|
|
result = self.xo_loc.apply_min_area(REAL_WEIGHTS, self.canvas_area)
|
|
self.assertEqual(set(result.floored),
|
|
{"xo-symboltable", "xo-printable2", "xo-pywebsock"})
|
|
|
|
def test_distortion_stays_tiny(self):
|
|
result = self.xo_loc.apply_min_area(REAL_WEIGHTS, self.canvas_area)
|
|
self.assertAlmostEqual(result.inflation, 0.000914, places=5) # ~0.09%
|
|
|
|
def test_INVARIANT_every_floored_tile_reaches_the_minimum_area(self):
|
|
# this is the property F was derived to guarantee
|
|
result = self.xo_loc.apply_min_area(REAL_WEIGHTS, self.canvas_area)
|
|
canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
placed = self.xo_loc.squarify(sorted(result.weights.items()), canvas)
|
|
for p in placed:
|
|
self.assertGreaterEqual(p.rect.w * p.rect.h,
|
|
self.xo_loc.A_MIN - 1e-6,
|
|
"%s is below the legible minimum" % p.key)
|
|
|
|
def test_too_many_subsystems_to_floor_is_reported_not_crashed(self):
|
|
# N * a >= 1: the canvas cannot give everyone a legible tile
|
|
weights = {"s%d" % i: 1 for i in range(2000)}
|
|
result = self.xo_loc.apply_min_area(weights, self.canvas_area)
|
|
self.assertEqual(result.weights, weights)
|
|
self.assertEqual(result.floored, [])
|
|
|
|
|
|
class TestLayout(unittest.TestCase):
|
|
# A two-subsystem fixture is too small for the production A_MIN to floor
|
|
# anything, so these pass an explicit min_tile_area chosen to exercise both
|
|
# branches deterministically: at 20000 px^2 the floor is 271 (> 109), so
|
|
# xo-symboltable is floored and lands at ~20601 px^2 -- above one sub-tile
|
|
# but below the 2 * 20000 needed to split it in two.
|
|
MIN_TILE = 20000.0
|
|
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
C = self.xo_loc.Cell
|
|
# Build is 25% of xo-reader2 here: it has to clear MIN_KIND_FRACTION to
|
|
# reach the legibility gate these tests are about.
|
|
self.groups = {
|
|
"xo-reader2": {"Code": C(9000, 620, 200), "Build": C(3000, 40, 20)},
|
|
"xo-symboltable": {"Code": C(76, 3, 4), "Build": C(33, 1, 2)},
|
|
}
|
|
|
|
def run_layout(self):
|
|
return self.xo_loc.layout(self.groups, self.canvas,
|
|
min_tile_area=self.MIN_TILE)
|
|
|
|
def test_big_tile_is_subdivided_by_kind(self):
|
|
tiles, _ = self.run_layout()
|
|
big = next(t for t in tiles if t.subsystem == "xo-reader2")
|
|
self.assertEqual({p.key for p in big.kind_rects}, {"Code", "Build"})
|
|
|
|
def test_floored_tile_is_NOT_subdivided(self):
|
|
# it is exactly one label big; splitting it would re-hide it
|
|
tiles, _ = self.run_layout()
|
|
small = next(t for t in tiles if t.subsystem == "xo-symboltable")
|
|
self.assertEqual(small.kind_rects, [])
|
|
self.assertEqual(small.dominant, "Code")
|
|
|
|
def test_sub_tiles_tile_their_parent_exactly(self):
|
|
tiles, _ = self.run_layout()
|
|
big = next(t for t in tiles if t.subsystem == "xo-reader2")
|
|
self.assertAlmostEqual(sum(p.rect.w * p.rect.h for p in big.kind_rects),
|
|
big.rect.w * big.rect.h, places=3)
|
|
|
|
def test_reports_the_floor_it_applied(self):
|
|
_, notes = self.run_layout()
|
|
self.assertTrue(any("min-area floor" in n for n in notes))
|
|
|
|
|
|
class TestKindSignificance(unittest.TestCase):
|
|
"""A kind earns a sub-tile by being a real FRACTION of its own subsystem.
|
|
|
|
An absolute area floor cannot express this: it scales with the subsystem,
|
|
so it keeps a 2% sliver in a big subsystem and drops a 30% share in a
|
|
small one. The fraction test also bounds tile shape -- a guillotine cut
|
|
gives a kind of fraction f an aspect of 1/(a*f), a >= 1 -- so the floor
|
|
doubles as the no-slivers guarantee.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
|
|
def aspect(self, rect):
|
|
return max(rect.w / rect.h, rect.h / rect.w)
|
|
|
|
def test_insignificant_kind_is_not_drawn(self):
|
|
# REGRESSION: xo-imgui is Code 97.9% / Build 2.1%. Drawn, Build was a
|
|
# 44.5:1 splinter -- and 2% build material is not a fact about the
|
|
# subsystem worth a tile.
|
|
#
|
|
# Nothing is hidden by flattening: the tile still covers the whole
|
|
# subsystem and its tooltip enumerates every kind, so this case needs
|
|
# no fold note. Only a SUBDIVIDED tile can swallow a kind silently.
|
|
C = self.xo_loc.Cell
|
|
groups = {"xo-imgui": {"Code": C(10778, 600, 150),
|
|
"Build": C(233, 10, 12, {"CMake": 233})}}
|
|
tiles, _ = self.xo_loc.layout(groups, self.canvas)
|
|
self.assertEqual(tiles[0].kind_rects, [])
|
|
self.assertEqual(tiles[0].dominant, "Code")
|
|
|
|
def test_a_fold_inside_a_subdivided_tile_is_reported(self):
|
|
# here the fold IS lossy: Docs gets no tile and no tooltip of its own,
|
|
# and its area is absorbed by Code and Build
|
|
C = self.xo_loc.Cell
|
|
groups = {"xo-b": {"Code": C(4300, 300, 60), "Build": C(700, 20, 10),
|
|
"Docs": C(90, 0, 3)}}
|
|
tiles, notes = self.xo_loc.layout(groups, self.canvas)
|
|
self.assertEqual({p.key for p in tiles[0].kind_rects}, {"Code", "Build"})
|
|
self.assertTrue(any("folded" in n for n in notes),
|
|
"a folded kind must be reported, never hidden")
|
|
|
|
def test_significant_kind_in_a_small_subsystem_is_kept(self):
|
|
# the point of a FRACTION floor: 30% Build is a real fact about this
|
|
# subsystem even though it is only 300 LOC
|
|
C = self.xo_loc.Cell
|
|
groups = {"xo-small": {"Code": C(700, 40, 9), "Build": C(300, 8, 5)}}
|
|
tiles, _ = self.xo_loc.layout(groups, self.canvas)
|
|
self.assertEqual({p.key for p in tiles[0].kind_rects}, {"Code", "Build"})
|
|
|
|
def test_INVARIANT_no_sub_tile_is_illegible_or_a_sliver(self):
|
|
C = self.xo_loc.Cell
|
|
groups = {
|
|
"xo-a": {"Code": C(10778, 600, 150), "Build": C(233, 10, 12)},
|
|
"xo-b": {"Code": C(4300, 300, 60), "Build": C(600, 20, 10),
|
|
"Docs": C(90, 0, 3)},
|
|
"xo-c": {"Build": C(3100, 90, 40), "Code": C(700, 30, 12)},
|
|
"xo-d": {"Code": C(76, 3, 4), "Build": C(33, 1, 2)},
|
|
}
|
|
tiles, _ = self.xo_loc.layout(groups, self.canvas)
|
|
drawn = 0
|
|
for tile in tiles:
|
|
for placement in tile.kind_rects:
|
|
drawn += 1
|
|
self.assertGreaterEqual(
|
|
placement.rect.w * placement.rect.h,
|
|
self.xo_loc.A_MIN - 1e-6,
|
|
"%s/%s is below the legible minimum"
|
|
% (tile.subsystem, placement.key))
|
|
self.assertLessEqual(
|
|
self.aspect(placement.rect), 8.0 + 1e-6,
|
|
"%s/%s is a sliver" % (tile.subsystem, placement.key))
|
|
self.assertGreater(drawn, 0, "the fixture must exercise subdivision")
|
|
|
|
def test_sub_tiles_still_tile_their_parent_exactly(self):
|
|
# folding redistributes the dropped area across the survivors; the
|
|
# parent must still be covered exactly
|
|
C = self.xo_loc.Cell
|
|
groups = {"xo-b": {"Code": C(4300, 300, 60), "Build": C(700, 20, 10),
|
|
"Docs": C(90, 0, 3)}}
|
|
tiles, _ = self.xo_loc.layout(groups, self.canvas)
|
|
tile = tiles[0]
|
|
self.assertAlmostEqual(
|
|
sum(p.rect.w * p.rect.h for p in tile.kind_rects),
|
|
tile.rect.w * tile.rect.h, places=3)
|
|
|
|
|
|
class TestFindRoot(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.tmp = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, self.tmp)
|
|
os.makedirs(os.path.join(self.tmp, "xo-cmake"))
|
|
os.makedirs(os.path.join(self.tmp, "xo-gc", "src", "gc"))
|
|
|
|
def test_explicit_root_wins(self):
|
|
self.assertEqual(self.xo_loc.find_root(explicit=self.tmp), self.tmp)
|
|
|
|
def test_env_is_used_when_no_explicit_root(self):
|
|
self.assertEqual(self.xo_loc.find_root(env=self.tmp), self.tmp)
|
|
|
|
def test_searches_upward_from_cwd(self):
|
|
deep = os.path.join(self.tmp, "xo-gc", "src", "gc")
|
|
self.assertEqual(self.xo_loc.find_root(cwd=deep), self.tmp)
|
|
|
|
def test_raises_when_no_root_can_be_found(self):
|
|
empty = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, empty)
|
|
with self.assertRaises(self.xo_loc.XoLocError):
|
|
self.xo_loc.find_root(cwd=empty)
|
|
|
|
|
|
class TestKnownSubsystems(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.tmp = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, self.tmp)
|
|
os.makedirs(os.path.join(self.tmp, "xo-gc"))
|
|
os.makedirs(os.path.join(self.tmp, "xo-cmake"))
|
|
os.makedirs(os.path.join(self.tmp, "pkgs"))
|
|
open(os.path.join(self.tmp, "xo-deps.svg"), "w").close()
|
|
|
|
def test_directories_matching_xo_star_only(self):
|
|
found = self.xo_loc.known_subsystems(self.tmp)
|
|
self.assertEqual(found, {"xo-gc", "xo-cmake"})
|
|
|
|
def test_excludes_a_FILE_matching_the_glob(self):
|
|
# xo-deps.svg is a real file in this repo root and matches xo-*
|
|
self.assertNotIn("xo-deps.svg", self.xo_loc.known_subsystems(self.tmp))
|
|
|
|
|
|
class TestRunScc(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
|
|
def test_missing_scc_raises_a_helpful_error(self):
|
|
with self.assertRaises(self.xo_loc.XoLocError) as ctx:
|
|
self.xo_loc.run_scc(".", scc="definitely-not-a-real-binary")
|
|
self.assertIn("scc", str(ctx.exception).lower())
|
|
|
|
def test_unrunnable_scc_raises_rather_than_tracebacks(self):
|
|
# REGRESSION: catching only FileNotFoundError let a present-but-not-
|
|
# executable --scc=PATH escape as a PermissionError traceback
|
|
tmp = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, tmp)
|
|
path = os.path.join(tmp, "not-executable")
|
|
with open(path, "w") as f:
|
|
f.write("#!/bin/sh\necho hi\n")
|
|
os.chmod(path, 0o644)
|
|
with self.assertRaises(self.xo_loc.XoLocError):
|
|
self.xo_loc.run_scc(".", scc=path)
|
|
|
|
|
|
REPO_ROOT = _HERE.parent.parent
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("scc"), "scc not installed")
|
|
class TestSccIntegration(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
|
|
def test_generated_marker_still_matches_the_tree(self):
|
|
# GUARD: the marker is coupled to xo-facet/codegen/*.j2 wording. If the
|
|
# templates are reflowed this fails loudly rather than silently
|
|
# counting generated code as hand-written.
|
|
raw = self.xo_loc.run_scc(str(REPO_ROOT))
|
|
rows = self.xo_loc.normalize(raw)
|
|
generated = [r for r in rows if r.generated]
|
|
self.assertGreater(
|
|
len(generated), 100,
|
|
"the %r marker matched %d files; has xo-facet/codegen/*.j2 changed?"
|
|
% (self.xo_loc.SCC_GENERATED_MARKER, len(generated)))
|
|
|
|
def test_root_walk_excludes_build_artifacts(self):
|
|
# walking a subdirectory instead would re-admit these
|
|
raw = self.xo_loc.run_scc(str(REPO_ROOT))
|
|
rows = self.xo_loc.normalize(raw)
|
|
self.assertEqual([r.path for r in rows if "/.build" in r.path], [])
|
|
|
|
def test_locations_resolve_to_real_subsystems(self):
|
|
# REGRESSION: this is the assertion that catches an absolute walk root
|
|
# leaking into Location. Without it the whole pipeline silently
|
|
# buckets every file under the subsystem '' and draws nothing.
|
|
raw = self.xo_loc.run_scc(str(REPO_ROOT))
|
|
rows = self.xo_loc.normalize(raw, root=str(REPO_ROOT))
|
|
found = {r.subsystem for r in rows}
|
|
self.assertIn("xo-gc", found)
|
|
self.assertIn("xo-cmake", found)
|
|
self.assertNotIn("", found)
|
|
|
|
|
|
class TestParser(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
self.parser = self.xo_loc.build_parser()
|
|
|
|
def test_defaults_match_the_spec(self):
|
|
args = self.parser.parse_args([])
|
|
self.assertEqual(args.format, "svg")
|
|
self.assertEqual(args.color, "kind")
|
|
self.assertEqual(args.min_loc, 20)
|
|
self.assertEqual(args.width, 1200)
|
|
self.assertEqual(args.height, 800)
|
|
self.assertFalse(args.include_generated)
|
|
self.assertFalse(args.include_vendored)
|
|
self.assertEqual(args.subsystems, [])
|
|
|
|
def test_positional_subsystems(self):
|
|
args = self.parser.parse_args(["xo-gc", "xo-arena"])
|
|
self.assertEqual(args.subsystems, ["xo-gc", "xo-arena"])
|
|
|
|
def test_kinds_is_split_on_commas(self):
|
|
args = self.parser.parse_args(["--kinds=Code,Build"])
|
|
self.assertEqual(self.xo_loc.resolve_args(args).kinds, ["Code", "Build"])
|
|
|
|
def test_listing_Generated_in_kinds_implies_include_generated(self):
|
|
args = self.xo_loc.resolve_args(
|
|
self.parser.parse_args(["--kinds=Code,Generated"]))
|
|
self.assertTrue(args.include_generated)
|
|
|
|
def test_png_is_rejected(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.parser.parse_args(["--format=png"])
|
|
|
|
def test_an_unknown_kind_is_named_rather_than_silently_empty(self):
|
|
# REGRESSION: --kinds=code (wrong case) used to yield the unhelpful
|
|
# "nothing to draw after filtering"
|
|
with self.assertRaises(self.xo_loc.XoLocError) as ctx:
|
|
self.xo_loc.resolve_args(self.parser.parse_args(["--kinds=code"]))
|
|
self.assertIn("code", str(ctx.exception))
|
|
self.assertIn("Code", str(ctx.exception)) # names the valid set
|
|
|
|
def test_valid_kinds_are_accepted(self):
|
|
args = self.xo_loc.resolve_args(
|
|
self.parser.parse_args(["--kinds=Code,Docs"]))
|
|
self.assertEqual(args.kinds, ["Code", "Docs"])
|
|
|
|
|
|
class TestTableFormats(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
C = self.xo_loc.Cell
|
|
self.groups = {
|
|
"xo-gc": {"Code": C(490, 49, 2), "Build": C(25, 1, 1)},
|
|
"xo-cmake": {"Build": C(1066, 30, 1)},
|
|
}
|
|
canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
self.tiles, _ = self.xo_loc.layout(self.groups, canvas)
|
|
self.args = self.xo_loc.build_parser().parse_args([])
|
|
|
|
def test_csv_has_a_row_per_subsystem_kind(self):
|
|
text = self.xo_loc.emit_csv(self.tiles, self.groups, [], self.args)
|
|
rows = list(csvmod.DictReader(io.StringIO(text)))
|
|
self.assertEqual(len(rows), 3)
|
|
gc_code = next(r for r in rows
|
|
if r["subsystem"] == "xo-gc" and r["kind"] == "Code")
|
|
self.assertEqual(gc_code["loc"], "490")
|
|
self.assertEqual(gc_code["complexity"], "49")
|
|
self.assertEqual(gc_code["nfiles"], "2")
|
|
|
|
def test_json_carries_totals_and_notes(self):
|
|
text = self.xo_loc.emit_json(self.tiles, self.groups,
|
|
["excluded 369 generated files"], self.args)
|
|
data = json.loads(text)
|
|
self.assertEqual(data["total_loc"], 1581)
|
|
self.assertEqual(data["subsystems"]["xo-gc"]["loc"], 515)
|
|
self.assertEqual(data["notes"], ["excluded 369 generated files"])
|
|
|
|
def test_json_reports_complexity_per_kloc(self):
|
|
text = self.xo_loc.emit_json(self.tiles, self.groups, [], self.args)
|
|
data = json.loads(text)
|
|
# xo-gc: 50 complexity over 515 loc
|
|
self.assertAlmostEqual(data["subsystems"]["xo-gc"]["cx_per_kloc"],
|
|
1000.0 * 50 / 515, places=3)
|
|
|
|
|
|
class TestMain(unittest.TestCase):
|
|
def test_reports_a_missing_scc_without_a_traceback(self):
|
|
# --root is pinned so this exercises the scc failure wherever it runs
|
|
# from; under ctest the cwd is the build tree, where root discovery
|
|
# would otherwise fail first and mask what this test is about.
|
|
xo_loc = load_xo_loc()
|
|
err = io.StringIO()
|
|
rc = xo_loc.main(["--scc=definitely-not-a-real-binary",
|
|
"--root=%s" % REPO_ROOT,
|
|
"--format=csv", "--output=/dev/null"], stderr=err)
|
|
self.assertEqual(rc, 1)
|
|
self.assertIn("scc", err.getvalue().lower())
|
|
self.assertNotIn("Traceback", err.getvalue())
|
|
|
|
def test_reports_an_undiscoverable_root_without_a_traceback(self):
|
|
xo_loc = load_xo_loc()
|
|
empty = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, empty)
|
|
cwd = os.getcwd()
|
|
os.chdir(empty)
|
|
self.addCleanup(os.chdir, cwd)
|
|
err = io.StringIO()
|
|
rc = xo_loc.main(["--format=csv", "--output=/dev/null"], stderr=err)
|
|
self.assertEqual(rc, 1)
|
|
self.assertIn("source root", err.getvalue())
|
|
self.assertNotIn("Traceback", err.getvalue())
|
|
|
|
|
|
class TestSvg(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
C = self.xo_loc.Cell
|
|
# Build is 25% of xo-reader2 and 30% of xo-symboltable, so both clear
|
|
# MIN_KIND_FRACTION and subdivide; xo-gc has a single kind.
|
|
self.groups = {
|
|
"xo-reader2": {"Code": C(9000, 620, 200), "Build": C(3000, 40, 20)},
|
|
"xo-gc": {"Code": C(6000, 650, 78)},
|
|
"xo-symboltable": {"Code": C(76, 3, 4), "Build": C(33, 1, 2)},
|
|
}
|
|
canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
self.tiles, self.notes = self.xo_loc.layout(self.groups, canvas)
|
|
self.args = self.xo_loc.build_parser().parse_args([])
|
|
|
|
def render(self, **kw):
|
|
for k, v in kw.items():
|
|
setattr(self.args, k, v)
|
|
return self.xo_loc.emit_svg(self.tiles, self.groups, self.notes, self.args)
|
|
|
|
def test_output_is_well_formed_xml(self):
|
|
root = ET.fromstring(self.render())
|
|
self.assertTrue(root.tag.endswith("svg"))
|
|
|
|
def test_declares_the_canvas_size(self):
|
|
root = ET.fromstring(self.render())
|
|
self.assertEqual(root.get("width"), "1200")
|
|
self.assertEqual(root.get("height"), "800")
|
|
|
|
def test_sets_an_explicit_background(self):
|
|
# xo-deps.in:196 records that inheriting the viewer's background
|
|
# renders elements near-invisible
|
|
root = ET.fromstring(self.render())
|
|
rects = root.iter("{http://www.w3.org/2000/svg}rect")
|
|
first = next(rects)
|
|
self.assertEqual(first.get("width"), "1200")
|
|
self.assertEqual(first.get("height"), "800")
|
|
self.assertTrue(first.get("fill"))
|
|
|
|
def test_draws_a_rect_per_leaf(self):
|
|
root = ET.fromstring(self.render())
|
|
rects = list(root.iter("{http://www.w3.org/2000/svg}rect"))
|
|
# At the production A_MIN this fixture floors nothing, so:
|
|
# background -> 1
|
|
# xo-reader2 Code 75% / Build 25%, 636148 px^2 -> 2
|
|
# xo-gc 1 kind -> flat -> 1
|
|
# xo-symboltable both kinds significant (70/30), but its
|
|
# own tile is 404.82 x 14.27 = 28.4:1, so
|
|
# no split of it can be squarish -> flat -> 1
|
|
self.assertEqual(len(rects), 1 + 2 + 1 + 1)
|
|
|
|
def test_single_kind_subsystem_is_never_subdivided(self):
|
|
root = ET.fromstring(self.render())
|
|
titles = [t.text for t in root.iter("{http://www.w3.org/2000/svg}title")]
|
|
self.assertEqual([t for t in titles if t.startswith("xo-gc")],
|
|
[t for t in titles if t.startswith("xo-gc:")])
|
|
|
|
def test_labels_the_subsystems(self):
|
|
text = self.render()
|
|
self.assertIn("xo-reader2", text)
|
|
self.assertIn("xo-gc", text)
|
|
|
|
def test_tooltip_carries_everything_the_spec_requires(self):
|
|
C = self.xo_loc.Cell
|
|
cell = C(490, 49, 2, {"C++": 400, "C++ Header": 90})
|
|
tip = self.xo_loc._tooltip("xo-gc", "Code", cell)
|
|
self.assertIn("490 LOC", tip) # loc
|
|
self.assertIn("cx 49", tip) # complexity
|
|
self.assertIn("100/kLOC", tip) # cx per kloc
|
|
self.assertIn("2 files", tip) # file count
|
|
self.assertIn("C++ 400", tip) # language breakdown
|
|
self.assertIn("C++ Header 90", tip)
|
|
|
|
def test_escapes_markup_in_labels(self):
|
|
self.assertEqual(self.xo_loc.svg_escape("a&b<c>"), "a&b<c>")
|
|
|
|
def test_kind_mode_colors_by_kind(self):
|
|
self.assertEqual(
|
|
self.xo_loc.tile_color("Build", 0.0, "kind"),
|
|
self.xo_loc.KIND_COLORS["Build"])
|
|
|
|
def test_complexity_mode_greys_out_non_code(self):
|
|
self.assertEqual(self.xo_loc.tile_color("Docs", 0.0, "complexity"),
|
|
self.xo_loc.NEUTRAL_COLOR)
|
|
self.assertNotEqual(self.xo_loc.tile_color("Code", 120.0, "complexity"),
|
|
self.xo_loc.NEUTRAL_COLOR)
|
|
|
|
def test_complexity_scale_is_monotonic(self):
|
|
low = self.xo_loc.tile_color("Code", 20.0, "complexity")
|
|
high = self.xo_loc.tile_color("Code", 160.0, "complexity")
|
|
self.assertNotEqual(low, high)
|
|
|
|
def test_every_kind_has_a_color(self):
|
|
# a kind with no slot would fall back to Other and silently lie
|
|
for kind in ["Code", "Build", "Docs", "Data", "Generated", "Other"]:
|
|
self.assertIn(kind, self.xo_loc.KIND_COLORS)
|
|
|
|
def test_kind_colors_are_distinct(self):
|
|
# two kinds sharing a hex would make the color dimension meaningless
|
|
hexes = list(self.xo_loc.KIND_COLORS.values())
|
|
self.assertEqual(len(hexes), len(set(hexes)))
|
|
|
|
|
|
class TestLabelContrast(unittest.TestCase):
|
|
"""Labels are judged against the tile they sit on, never the canvas.
|
|
|
|
A fixed label colour cannot work across this palette: white on Generated
|
|
is 2.35:1 and white on the pale end of the complexity ramp is 1.54:1,
|
|
while ink on Build is 2.41:1.
|
|
"""
|
|
|
|
WCAG_LARGE_TEXT = 3.0
|
|
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
|
|
def test_contrast_ratio_matches_wcag(self):
|
|
# white on black is the definitional 21:1; a colour against itself 1:1
|
|
self.assertAlmostEqual(
|
|
self.xo_loc.contrast_ratio("#ffffff", "#000000"), 21.0, places=2)
|
|
self.assertAlmostEqual(
|
|
self.xo_loc.contrast_ratio("#2a78d6", "#2a78d6"), 1.0, places=6)
|
|
|
|
def test_every_kind_fill_gets_a_readable_label(self):
|
|
for kind, fill in self.xo_loc.KIND_COLORS.items():
|
|
chosen, ratio = self.xo_loc.label_color(fill)
|
|
self.assertGreaterEqual(
|
|
ratio, self.WCAG_LARGE_TEXT,
|
|
"%s label on %s is only %.2f:1" % (kind, fill, ratio))
|
|
|
|
def test_every_complexity_shade_gets_a_readable_label(self):
|
|
for shade in list(self.xo_loc.CX_RAMP) + [self.xo_loc.NEUTRAL_COLOR]:
|
|
chosen, ratio = self.xo_loc.label_color(shade)
|
|
self.assertGreaterEqual(
|
|
ratio, self.WCAG_LARGE_TEXT,
|
|
"label on %s is only %.2f:1" % (shade, ratio))
|
|
|
|
def test_pale_fills_take_ink_and_dark_fills_take_white(self):
|
|
self.assertEqual(self.xo_loc.label_color("#b49ee3")[0],
|
|
self.xo_loc.LABEL_INK)
|
|
self.assertEqual(self.xo_loc.label_color("#843826")[0],
|
|
self.xo_loc.LABEL_ON_DARK)
|
|
|
|
def test_label_is_judged_on_the_worst_fill_it_covers(self):
|
|
# ink is fine on the pale Generated fill (8.38:1) but poor on the rust
|
|
# Build fill (2.41:1); spanning both, neither colour may be assumed
|
|
ink, worst = self.xo_loc.label_color(
|
|
self.xo_loc.KIND_COLORS["Generated"], self.xo_loc.KIND_COLORS["Build"])
|
|
self.assertAlmostEqual(
|
|
worst,
|
|
min(self.xo_loc.contrast_ratio(ink, self.xo_loc.KIND_COLORS["Generated"]),
|
|
self.xo_loc.contrast_ratio(ink, self.xo_loc.KIND_COLORS["Build"])),
|
|
places=6)
|
|
|
|
def test_INVARIANT_every_drawn_label_clears_the_minimum_on_every_fill(self):
|
|
# REGRESSION: the label box is 64 px wide but is gated on the PARENT
|
|
# tile, so on a narrow first sub-tile it can straddle two fills. It
|
|
# must be readable on all of them, or not drawn at all.
|
|
C = self.xo_loc.Cell
|
|
groups = {
|
|
"xo-a": {"Code": C(9000, 600, 150), "Build": C(3000, 90, 40)},
|
|
"xo-b": {"Generated": C(2000, 10, 30), "Build": C(1800, 60, 25)},
|
|
"xo-c": {"Code": C(700, 40, 9), "Docs": C(300, 0, 5)},
|
|
}
|
|
canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
args = self.xo_loc.build_parser().parse_args(["--include-generated"])
|
|
tiles, notes = self.xo_loc.layout(groups, canvas)
|
|
svg = self.xo_loc.emit_svg(tiles, groups, notes, args)
|
|
|
|
root = ET.fromstring(svg)
|
|
NS = "{http://www.w3.org/2000/svg}"
|
|
rects = [r for r in root.iter(NS + "rect")][1:] # drop background
|
|
boxes = [(self.xo_loc.Rect(*(float(r.get(k))
|
|
for k in ("x", "y", "width", "height"))),
|
|
r.get("fill")) for r in rects]
|
|
|
|
drawn = 0
|
|
for text in root.iter(NS + "text"):
|
|
drawn += 1
|
|
x, y = float(text.get("x")), float(text.get("y"))
|
|
box = self.xo_loc.Rect(x, y - 11, self.xo_loc.LABEL_W,
|
|
self.xo_loc.LABEL_H)
|
|
touched = [f for rect, f in boxes if self.xo_loc._overlaps(box, rect)]
|
|
for fill in touched:
|
|
ratio = self.xo_loc.contrast_ratio(text.get("fill"), fill)
|
|
self.assertGreaterEqual(
|
|
ratio, self.WCAG_LARGE_TEXT,
|
|
"label %r sits on %s at only %.2f:1"
|
|
% (text.text, fill, ratio))
|
|
self.assertGreater(drawn, 0, "the fixture must draw some labels")
|
|
|
|
|
|
class TestHtml(unittest.TestCase):
|
|
def setUp(self):
|
|
self.xo_loc = load_xo_loc()
|
|
C = self.xo_loc.Cell
|
|
self.groups = {
|
|
"xo-reader2": {"Code": C(9000, 620, 200), "Build": C(3000, 40, 20)},
|
|
"xo-gc": {"Code": C(6000, 650, 78)},
|
|
}
|
|
canvas = self.xo_loc.Rect(0.0, 0.0, 1200.0, 800.0)
|
|
self.tiles, self.notes = self.xo_loc.layout(self.groups, canvas)
|
|
self.args = self.xo_loc.build_parser().parse_args([])
|
|
self.html = self.xo_loc.emit_html(self.tiles, self.groups,
|
|
self.notes, self.args)
|
|
|
|
def test_is_a_complete_document(self):
|
|
self.assertTrue(self.html.lstrip().startswith("<!doctype html>"))
|
|
self.assertIn("</html>", self.html)
|
|
|
|
def test_embeds_the_svg(self):
|
|
self.assertIn("<svg", self.html)
|
|
self.assertIn("xo-reader2", self.html)
|
|
|
|
def test_is_self_contained(self):
|
|
# no CDN, no external stylesheet, no remote font
|
|
for forbidden in ("http://", "https://cdn", "<link", "src=\"http"):
|
|
self.assertNotIn(forbidden, self.html.replace(
|
|
"http://www.w3.org/2000/svg", ""))
|
|
|
|
def test_carries_the_exclusion_report_visibly(self):
|
|
notes = ["excluded 369 generated files (10968 LOC)"]
|
|
html = self.xo_loc.emit_html(self.tiles, self.groups, notes, self.args)
|
|
# the report must be visible in the page, not just an svg comment
|
|
self.assertIn("excluded 369 generated files", html)
|
|
self.assertIn('class="notes"', html)
|
|
|
|
def test_ships_a_legend(self):
|
|
self.assertIn('class="legend"', self.html)
|
|
self.assertIn("Build", self.html)
|
|
|
|
def test_handles_both_color_schemes(self):
|
|
self.assertIn("prefers-color-scheme", self.html)
|
|
|
|
def test_has_the_interaction_hooks(self):
|
|
self.assertIn("addEventListener", self.html)
|
|
|
|
def test_complexity_mode_legend_describes_the_ramp(self):
|
|
args = self.xo_loc.build_parser().parse_args(["--color=complexity"])
|
|
html = self.xo_loc.emit_html(self.tiles, self.groups, [], args)
|
|
self.assertIn("cx", html)
|
|
self.assertIn(self.xo_loc.NEUTRAL_COLOR, html)
|
|
|
|
def test_escapes_markup_coming_from_notes(self):
|
|
html = self.xo_loc.emit_html(self.tiles, self.groups,
|
|
["<script>alert(1)</script>"], self.args)
|
|
self.assertNotIn("<script>alert(1)</script>", html)
|
|
self.assertIn("<script>", html)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|