import os
import re
import shutil
import tempfile
import unittest
from xml.etree import ElementTree
import build
import buildtest
import coverage
def _project_xml(text):
return build.parse_project_xml(text.encode("utf-8"))
def _repo_path(*parts):
return os.path.join(build.REPO_ROOT, *parts)
_SAMPLE_COBERTURA = """
"""
class ModuleOfTests(unittest.TestCase):
def test_returns_the_module_folder_for_a_deeply_nested_project(self):
path = _repo_path("Modules", "Blacklight", "Core", "Core.UnitTests", "Blacklight.Core.UnitTests.csproj")
self.assertEqual(_repo_path("Modules", "Blacklight"), coverage.module_of(path))
def test_returns_the_game_folder_for_a_game_project(self):
path = _repo_path("Games", "ElectricRootPrototype", "ElectricRootPrototype", "ElectricRootPrototype.csproj")
self.assertEqual(_repo_path("Games", "ElectricRootPrototype"), coverage.module_of(path))
class ReadPropertiesTests(unittest.TestCase):
def test_keeps_the_first_definition_of_a_property(self):
xml = _project_xml("first"
"second")
self.assertEqual("first", coverage.read_properties(xml)["OutputPath"])
def test_skips_an_empty_definition_in_favor_of_a_later_one(self):
xml = _project_xml(" "
"second")
self.assertEqual("second", coverage.read_properties(xml)["OutputPath"])
class ExpandPropertiesTests(unittest.TestCase):
def test_expands_references_inside_other_properties_values(self):
properties = {"OutputPath": r"$(Root)\Bin", "Root": "C:"}
self.assertEqual(r"C:\Bin\x.exe", coverage.expand_properties(r"$(OutputPath)\x.exe", properties))
def test_raises_naming_an_undefined_property(self):
with self.assertRaisesRegex(ValueError, re.escape("$(Missing)")):
coverage.expand_properties("$(Missing)", {})
def test_raises_for_a_self_referencing_property(self):
with self.assertRaisesRegex(ValueError, "without end"):
coverage.expand_properties("$(Loop)", {"Loop": "$(Loop)"})
class FindTestCommandTests(unittest.TestCase):
def test_finds_a_quoted_post_build_event_running_the_target(self):
xml = _project_xml('"$(TargetPath)"'
'')
self.assertEqual('"$(TargetPath)"', coverage.find_test_command(xml))
def test_finds_an_unquoted_post_build_event_with_arguments(self):
xml = _project_xml('$(TargetPath) "$(ProjectDir)Shaders"'
'')
self.assertEqual('$(TargetPath) "$(ProjectDir)Shaders"', coverage.find_test_command(xml))
def test_finds_an_exec_running_the_exe_after_build(self):
xml = _project_xml(''
'')
self.assertEqual('"$(TargetDir)$(AssemblyName).exe"', coverage.find_test_command(xml))
def test_ignores_an_exec_in_a_target_not_run_after_build(self):
xml = _project_xml(''
'')
self.assertIsNone(coverage.find_test_command(xml))
def test_ignores_a_post_build_event_running_something_else(self):
xml = _project_xml('echo $(TargetPath)>out.txt'
'')
self.assertIsNone(coverage.find_test_command(xml))
def test_ignores_a_target_path_prefix_of_a_longer_property(self):
xml = _project_xml('$(TargetPathWithTargetPlatformMoniker)'
'')
self.assertIsNone(coverage.find_test_command(xml))
def test_returns_none_for_a_project_with_no_build_events(self):
self.assertIsNone(coverage.find_test_command(_project_xml("")))
class MakeSuiteTests(unittest.TestCase):
_CSPROJ_PATH = _repo_path("Modules", "Glass", "Glass.UnitTests", "Glass.UnitTests.csproj")
def test_locates_the_executable_through_the_output_path(self):
xml = _project_xml(r'$(SourceRoot)\Junk\$(AssemblyName)\$(Configuration)\\'
r'"$(TargetPath)"')
suite = coverage.make_suite("Glass.UnitTests", self._CSPROJ_PATH, xml)
expected = _repo_path("Junk", "Glass.UnitTests", "Release", "Glass.UnitTests.exe")
self.assertEqual(expected, suite.executable)
self.assertEqual([expected], suite.command)
def test_resolves_an_output_path_relative_to_the_project(self):
xml = _project_xml(r'bin\$(Configuration)'
r'"$(TargetPath)"')
suite = coverage.make_suite("Glass.UnitTests", self._CSPROJ_PATH, xml)
self.assertEqual(_repo_path("Modules", "Glass", "Glass.UnitTests", "bin", "Release", "Glass.UnitTests.exe"),
suite.executable)
def test_expands_and_unquotes_the_arguments(self):
xml = _project_xml(r'bin'
r'"$(TargetPath)" "$(ProjectDir)My Shaders"'
r'')
suite = coverage.make_suite("Glass.UnitTests", self._CSPROJ_PATH, xml)
self.assertEqual(_repo_path("Modules", "Glass", "Glass.UnitTests") + os.sep + "My Shaders", suite.command[1])
def test_belongs_to_the_module_its_project_lives_in(self):
xml = _project_xml(r'bin'
r'"$(TargetPath)"')
suite = coverage.make_suite("Glass.UnitTests", self._CSPROJ_PATH, xml)
self.assertEqual(_repo_path("Modules", "Glass"), suite.module)
def test_returns_none_for_a_project_whose_build_runs_no_tests(self):
xml = _project_xml("bin")
self.assertIsNone(coverage.make_suite("Glass", self._CSPROJ_PATH, xml))
class ModuleAssemblyNamesTests(unittest.TestCase):
def test_returns_the_modules_projects_except_its_suites(self):
glass = _repo_path("Modules", "Glass")
projects = {
"Glass": build.Project("Glass", os.path.join(glass, "Glass"), "Glass.csproj"),
"Glass.Blacklight": build.Project("Glass.Blacklight", os.path.join(glass, "Glass.Blacklight"),
"Glass.Blacklight.csproj"),
"Glass.UnitTests": build.Project("Glass.UnitTests", os.path.join(glass, "Glass.UnitTests"),
"Glass.UnitTests.csproj"),
"Amphora": build.Project("Amphora", _repo_path("Modules", "Amphora", "Amphora"), "Amphora.csproj"),
}
self.assertEqual(["Glass", "Glass.Blacklight"],
coverage.module_assembly_names(projects, glass, {"Glass.UnitTests"}))
class SettingsXmlTests(unittest.TestCase):
def _pattern(self, assembly_names):
return ElementTree.fromstring(coverage.settings_xml(assembly_names)).findtext(
"CodeCoverage/ModulePaths/Include/ModulePath")
def test_matches_a_named_dll_or_exe_in_any_directory(self):
pattern = self._pattern(["Glass", "ElectricRootPrototype"])
self.assertTrue(re.match(pattern, r"C:\ElectricRoot\Junk\Glass.UnitTests\Release\Glass.dll"))
self.assertTrue(re.match(pattern, r"C:\ElectricRoot\Junk\X\Release\ElectricRootPrototype.exe"))
def test_rejects_an_assembly_that_merely_starts_with_a_name(self):
pattern = self._pattern(["Glass"])
self.assertFalse(re.match(pattern, r"C:\ElectricRoot\Bin\Release\Glass.UnitTests.dll"))
def test_escapes_the_dots_in_a_name(self):
pattern = self._pattern(["Glass.Blacklight"])
self.assertFalse(re.match(pattern, r"C:\ElectricRoot\Bin\Release\GlassXBlacklight.dll"))
class IsPinnedPackageTests(unittest.TestCase):
def test_rejects_any_other_bytes(self):
self.assertFalse(coverage.is_pinned_package(b"not the package"))
_X64_ONLY_COBERTURA = """
"""
class UnionTests(unittest.TestCase):
def union(self):
return coverage.union([ElementTree.fromstring(_SAMPLE_COBERTURA), ElementTree.fromstring(_X64_ONLY_COBERTURA)])
def label_lines(self, root):
return {line.get("number"): line for line in root.find(
"packages/package[@name='Glass']/classes/class[@name='Glass.Label']").findall("lines/line")}
def test_hits_add_up_so_a_line_either_ran_counts(self):
self.assertEqual("2", self.label_lines(self.union())["11"].get("hits"))
def test_a_line_only_one_has_is_kept(self):
self.assertEqual("1", self.label_lines(self.union())["12"].get("hits"))
def test_a_lines_branches_taken_are_the_most_either_took(self):
line = self.label_lines(self.union())["10"]
self.assertEqual("True", line.get("branch"))
self.assertEqual((1, 2), coverage.condition_counts(line))
def test_a_line_with_no_branches_says_so(self):
line = self.label_lines(self.union())["11"]
self.assertEqual("False", line.get("branch"))
self.assertIsNone(line.get("condition-coverage"))
def test_a_method_that_ran_in_either_is_covered(self):
methods = coverage.uncovered_methods(self.union(), ["Glass"])
self.assertEqual(["get_Text"], [method.method_name for method in methods])
def test_keeps_every_package_class_and_method_either_has(self):
summaries = coverage.summarize(self.union())
self.assertEqual([coverage.AssemblyCoverage("Glass", lines_covered=4, lines_valid=5, branches_covered=3,
branches_valid=4, methods_covered=2, methods_total=4),
coverage.AssemblyCoverage("Glass.Blacklight", lines_covered=0, lines_valid=1,
branches_covered=0, branches_valid=0, methods_covered=0,
methods_total=1)],
summaries)
def test_one_document_is_unchanged_in_what_it_covers(self):
self.assertEqual(coverage.summarize(ElementTree.fromstring(_SAMPLE_COBERTURA)),
coverage.summarize(coverage.union([ElementTree.fromstring(_SAMPLE_COBERTURA)])))
def test_keeps_method_signatures(self):
method = self.union().find(".//method[@name='Draw']")
self.assertEqual("(Glass.Canvas)", method.get("signature"))
class OtherBitnessCommandTests(unittest.TestCase):
def test_points_every_argument_naming_the_exe_at_the_copy(self):
self.assertEqual([r"C:\Out\Suite.x64.exe", r"C:\Shaders", r"C:\Out\Suite.x64.exe"],
coverage.other_bitness_command([r"C:\Out\Suite.exe", r"C:\Shaders", r"c:\out\SUITE.EXE"],
r"C:\Out\Suite.exe", r"C:\Out\Suite.x64.exe"))
class MakeOtherBitnessCopyTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.directory)
self.executable = os.path.join(self.directory, "Suite.exe")
def write_exe(self, flags):
with open(self.executable, "wb") as file:
file.write(buildtest._make_image(flags))
def test_copies_the_exe_flipped_to_the_other_bitness(self):
self.write_exe(buildtest._any_cpu_preferring_32bit())
label, copy_path = coverage.make_other_bitness_copy(self.executable)
self.assertEqual("x64", label)
self.assertEqual(os.path.join(self.directory, "Suite.x64.exe"), copy_path)
with open(copy_path, "rb") as file:
self.assertEqual(buildtest._make_image(build.CLR_ILONLY), file.read())
def test_brings_the_config_along(self):
self.write_exe(buildtest._any_cpu_preferring_32bit())
with open(self.executable + ".config", "w", encoding="utf-8") as file:
file.write("")
label, copy_path = coverage.make_other_bitness_copy(self.executable)
with open(copy_path + ".config", encoding="utf-8") as file:
self.assertEqual("", file.read())
def test_leaves_the_original_alone(self):
self.write_exe(buildtest._any_cpu_preferring_32bit())
coverage.make_other_bitness_copy(self.executable)
with open(self.executable, "rb") as file:
self.assertEqual(buildtest._make_image(buildtest._any_cpu_preferring_32bit()), file.read())
def test_an_exe_pinned_to_one_bitness_gets_no_copy(self):
self.write_exe(build.CLR_ILONLY | build.CLR_32BIT_REQUIRED)
self.assertIsNone(coverage.make_other_bitness_copy(self.executable))
self.assertEqual(["Suite.exe"], os.listdir(self.directory))
class SummarizeTests(unittest.TestCase):
def test_counts_lines_branches_and_methods_per_assembly(self):
summaries = coverage.summarize(ElementTree.fromstring(_SAMPLE_COBERTURA))
self.assertEqual([coverage.AssemblyCoverage("Glass", lines_covered=1, lines_valid=3, branches_covered=1,
branches_valid=2, methods_covered=1, methods_total=3),
coverage.AssemblyCoverage("Glass.Blacklight", lines_covered=0, lines_valid=1,
branches_covered=0, branches_valid=0, methods_covered=0,
methods_total=1)],
summaries)
class UncoveredMethodsTests(unittest.TestCase):
def test_lists_only_methods_with_lines_none_of_which_ran(self):
methods = coverage.uncovered_methods(ElementTree.fromstring(_SAMPLE_COBERTURA), ["Glass"])
self.assertEqual([coverage.UncoveredMethod(r"C:\ElectricRoot\Modules\Glass\Glass\Label.cs", 20, "Glass.Label",
"get_Text", "()")],
methods)
def test_skips_assemblies_not_named(self):
methods = coverage.uncovered_methods(ElementTree.fromstring(_SAMPLE_COBERTURA), ["Glass.Blacklight"])
self.assertEqual(["Flush"], [method.method_name for method in methods])
class UnloadedLibrariesTests(unittest.TestCase):
def test_returns_the_measured_modules_libraries_missing_from_the_results(self):
oculus = _repo_path("Modules", "Oculus")
projects = {
"Oculus": build.Project("Oculus", os.path.join(oculus, "Oculus"), "Oculus.csproj"),
"Oculus.Measured": build.Project("Oculus.Measured", os.path.join(oculus, "Measured"), "Measured.csproj"),
"Oculus.Testbed": build.Project("Oculus.Testbed", os.path.join(oculus, "Testbed"), "Testbed.csproj"),
"Amphora": build.Project("Amphora", _repo_path("Modules", "Amphora", "Amphora"), "Amphora.csproj"),
}
for name in ("Oculus", "Oculus.Measured", "Amphora"):
projects[name].is_library = True
summaries = [coverage.AssemblyCoverage("Oculus.Measured", 1, 1, 0, 0, 1, 1)]
self.assertEqual(["Oculus"], coverage.unloaded_libraries(projects, [oculus], summaries))
class FormatRatioTests(unittest.TestCase):
def test_shows_the_percentage_and_counts(self):
self.assertEqual(" 50.0% (1/2)", coverage.format_ratio(1, 2))
def test_shows_a_dash_when_there_is_nothing_to_cover(self):
self.assertEqual("-", coverage.format_ratio(0, 0))
if __name__ == '__main__':
unittest.main()