Files
RollCalcPython/tests/test_roll_calculation.py
T

782 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import math
import unittest
from roll_calculation import (
ArticleRepository,
CalculationRequest,
CalculationState,
_canonical_name,
article_production_site,
calculate_material_weight,
calculate_product_length,
calculate_roll,
get_article,
modify_calculation,
transport_roll_inputs_from_roll_calculation,
)
def representative_request(**changes):
payload = {
"article_number": "146900",
"roll_length_m": 50.0,
"width_m": 5.8,
"core_diameter_mm": 150.0,
"include_roll_weight": True,
}
payload.update(changes)
return payload
class RollCalculationTests(unittest.TestCase):
def setUp(self):
self.repository = ArticleRepository(
[
{
"nr": "00123",
"name": "Alpha Product, 4,90 x 20 m",
"thickness": 2.0,
"thickness_stddev": 0.1,
"area_weight": 1000.0,
"core_type": 0.0,
},
{
"nr": "00999",
"name": "Product Without Width",
"thickness": 3.0,
"thickness_stddev": 0.0,
"area_weight": 900.0,
"core_type": 0.0,
},
{
"nr": "01000",
"name": "Duplicated Product",
"thickness": 3.0,
"thickness_stddev": 0.0,
"area_weight": 900.0,
"core_type": 0.0,
},
{
"nr": "01001",
"name": "Duplicated Product",
"thickness": 3.0,
"thickness_stddev": 0.0,
"area_weight": 900.0,
"core_type": 0.0,
},
{
"nr": "02000",
"name": "Shared Product, 4,90 x 20 m",
"thickness": 2.0,
"thickness_stddev": 0.1,
"area_weight": 1000.0,
"core_type": 0.0,
},
{
"nr": "02001",
"name": "Shared Product, 5,00 x 40 m",
"thickness": 2.0,
"thickness_stddev": 0.1,
"area_weight": 1000.0,
"core_type": 0.0,
},
{
"nr": "03000",
"name": "Bfix Alias Product",
"thickness": 2.0,
"thickness_stddev": 0.1,
"area_weight": 1000.0,
"core_type": 0.0,
},
]
)
def calculate_fixture(self, payload):
return calculate_roll(payload, article_repository=self.repository)
def test_public_get_article_uses_repository_resolution_and_projection(self):
result = get_article("00123", article_repository=self.repository)
self.assertEqual(result["status"], "resolved")
self.assertEqual(result["article"]["number"], "00123")
self.assertEqual(result["article"]["width_m"], 4.9)
self.assertEqual(result["article"]["area_weight_g_m2"], 1000.0)
def test_public_get_article_preserves_not_found_ambiguous_and_conflict_results(self):
self.assertEqual(
get_article("does-not-exist", article_repository=self.repository)["status"],
"article_not_found",
)
self.assertEqual(
get_article(
article_name_hint="Duplicated Product",
article_repository=self.repository,
)["status"],
"article_ambiguous",
)
self.assertEqual(
get_article(
"00123",
"Other product",
article_repository=self.repository,
)["status"],
"article_conflict",
)
def test_headless_matches_representative_existing_ui_values(self):
result = calculate_roll(representative_request())
calculation = result["calculation"]
self.assertEqual(result["status"], "success")
self.assertAlmostEqual(calculation["minimum_diameter_mm"], 813.0877908536811)
self.assertAlmostEqual(calculation["average_diameter_mm"], 845.8528172323103)
self.assertAlmostEqual(calculation["maximum_diameter_mm"], 877.3951340213555)
self.assertEqual(calculation["effective_roll_length_m"], 50.0)
self.assertAlmostEqual(calculation["roll_weight_kg"], 512.4909)
def test_transport_roll_inputs_match_browser_average_and_display_rounding(self):
result = calculate_roll(representative_request())
self.assertEqual(
transport_roll_inputs_from_roll_calculation(result),
{
"roll_diameter_mm": 845.9,
"core_diameter_mm": 150.0,
"roll_width_m": 5.8,
"roll_weight_kg": 512.5,
"product_length_m": 50.0,
},
)
def test_transport_roll_inputs_use_browser_equivalent_half_rounding(self):
result = {
"status": "success",
"calculation": {
"average_diameter_mm": 1000.25,
"roll_weight_kg": 12.25,
"effective_roll_length_m": 10.0,
},
"effective_inputs": {
"core_diameter_mm": {"value": 150.0},
"width_m": {"value": 2.0},
},
}
inputs = transport_roll_inputs_from_roll_calculation(result)
self.assertEqual(inputs["roll_diameter_mm"], 1000.3)
self.assertEqual(inputs["roll_weight_kg"], 12.3)
def test_transport_roll_inputs_reject_unsuccessful_or_incomplete_results(self):
for result in (
{"status": "needs_clarification"},
{"status": "success", "calculation": {}, "effective_inputs": {}},
):
with self.subTest(result=result):
with self.assertRaises(ValueError):
transport_roll_inputs_from_roll_calculation(result)
def test_optional_weight_is_omitted_without_requesting_it(self):
result = calculate_roll(
representative_request(width_m=None, include_roll_weight=False)
)
self.assertEqual(result["status"], "success")
self.assertIsNone(result["calculation"]["roll_weight_kg"])
def test_material_weight_does_not_require_or_assume_a_core_diameter(self):
result = calculate_material_weight(
roll_length_m=60,
width_m=5.0,
area_weight_g_m2=4850.079676,
)
self.assertEqual(result["status"], "success")
self.assertEqual(result["weight_scope"], "material_only")
self.assertAlmostEqual(result["material_weight_kg"], 1455.0239028)
self.assertNotIn("core_diameter_mm", result["inputs"])
def test_roll_diameter_still_rejects_a_zero_core_diameter(self):
result = calculate_roll(
{
"roll_length_m": 60,
"width_m": 5.0,
"area_weight_g_m2": 4850.079676,
"thickness_mm": 6.461044,
"core_diameter_mm": 0,
"include_roll_weight": True,
}
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertIn("core_diameter_mm", [
item["field"] for item in result["invalid"]
])
def test_product_length_characterizes_article_218500_with_ranges_and_weight(self):
result = calculate_product_length(
target_roll_diameter_mm=600,
core_diameter_mm=140,
thickness_mm=4.311611,
thickness_stddev_mm=0.249999,
width_m=6.0,
area_weight_g_m2=495.395833,
)
self.assertEqual(result["status"], "success")
calculation = result["calculation"]
self.assertAlmostEqual(calculation["minimum_product_length_m"], 55.5634372661)
self.assertAlmostEqual(calculation["average_product_length_m"], 62.0068774341)
self.assertAlmostEqual(calculation["maximum_product_length_m"], 70.1407868061)
self.assertAlmostEqual(
calculation["material_weights_kg"]["average_kg"], 184.3076921892
)
def test_product_length_rejects_target_diameter_not_larger_than_core(self):
result = calculate_product_length(
target_roll_diameter_mm=140,
core_diameter_mm=140,
thickness_mm=4,
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertEqual(
result["invalid"][0]["field"], "target_roll_diameter_mm"
)
def test_diameter_only_calculation_does_not_require_width(self):
result = self.calculate_fixture(
{
"article_number": "00999",
"roll_length_m": 20,
"core_diameter_mm": 150,
"include_roll_weight": False,
}
)
self.assertEqual(result["status"], "success")
self.assertNotIn("width_m", result["effective_inputs"])
self.assertIsNone(result["calculation"]["roll_weight_kg"])
def test_missing_core_diameter_blocks_diameter_calculation(self):
result = self.calculate_fixture(
{
"article_number": "00999",
"roll_length_m": 20,
"include_roll_weight": False,
}
)
self.assertEqual(result["status"], "needs_clarification")
self.assertIn("core_diameter_mm", result["missing"])
self.assertNotIn("width_m", result["missing"])
def test_warning_and_note_rules_are_preserved(self):
heavy = calculate_roll(
{
"roll_length_m": 100,
"width_m": 4,
"thickness_mm": 5,
"area_weight_g_m2": 5000,
"core_diameter_mm": 150,
"category": "bentofix",
"include_roll_weight": True,
}
)
normal = calculate_roll(representative_request(category="bentofix"))
self.assertEqual(heavy["warnings"][0]["severity"], "warning")
self.assertIn("min 170 mm required", heavy["warnings"][0]["message"])
self.assertIn("within standard limits", normal["notes"][0])
def test_exact_article_number_preserves_leading_zeroes(self):
result = self.calculate_fixture(
{"article_number": "00123", "roll_length_m": 20, "core_diameter_mm": 150}
)
self.assertEqual(result["status"], "success")
self.assertEqual(result["resolved_article"]["number"], "00123")
numeric = self.calculate_fixture(
{"article_number": 123, "roll_length_m": 20, "core_diameter_mm": 150}
)
self.assertEqual(numeric["status"], "invalid_parameter")
def test_exact_normalized_article_name_resolves_without_number(self):
result = self.calculate_fixture({
"article_name_hint": "Alpha Product, 4,90 x 20 m",
"roll_length_m": 20,
"core_diameter_mm": 150,
})
self.assertEqual(result["status"], "success")
self.assertEqual(result["resolved_article"]["number"], "00123")
def test_article_name_matching_is_case_insensitive(self):
result = self.calculate_fixture({
"article_name_hint": "pRoDuCt WiThOuT wIdTh",
"roll_length_m": 20,
"core_diameter_mm": 150,
})
self.assertEqual(result["status"], "success")
self.assertEqual(result["resolved_article"]["number"], "00999")
def test_article_name_normalizes_safe_punctuation_and_whitespace(self):
result = self.calculate_fixture({
"article_name_hint": " Alpha---Product ; 4.90 × 20 M ",
"roll_length_m": 20,
"core_diameter_mm": 150,
})
self.assertEqual(result["status"], "success")
self.assertEqual(result["resolved_article"]["number"], "00123")
def test_model_designator_spacing_matches_the_same_article_family(self):
repository = ArticleRepository([
{"nr": "04000", "name": "Stex R 501, 5,20 x 100 m"},
{"nr": "04001", "name": "Stex R 501, 6,00 x 100 m"},
])
resolution = repository.resolve(None, "Stex R501")
self.assertEqual(resolution["status"], "article_ambiguous")
self.assertEqual(
[candidate["article_number"] for candidate in resolution["candidates"]],
["04000", "04001"],
)
def test_base_name_ignores_dimensions_and_parenthetical_descriptor(self):
repository = ArticleRepository([
{
"nr": "04000",
"name": "Stex R 501 (Betonvlies), 5,20 x 100 m",
},
])
resolution = repository.resolve(None, "Stex R-501")
self.assertEqual(resolution["status"], "resolved")
self.assertEqual(resolution["article"]["number"], "04000")
def test_secutex_and_stex_r501_have_the_same_ambiguous_candidates(self):
repository = ArticleRepository([
{"nr": "04000", "name": "Stex R 501, 5,20 x 100 m"},
{"nr": "04001", "name": "Stex R 501, 6,00 x 100 m"},
])
spaced = repository.resolve(None, "Secutex R 501")
compact = repository.resolve(None, "sEcUtEx R501")
stored = repository.resolve(None, "Stex R501")
self.assertEqual(spaced["status"], "article_ambiguous")
self.assertEqual(compact["status"], "article_ambiguous")
self.assertEqual(
spaced["candidates"],
stored["candidates"],
)
self.assertEqual(compact["candidates"], stored["candidates"])
def test_bentofix_and_bfix_resolve_equivalently(self):
repository = ArticleRepository([
{"nr": "04000", "name": "Bfix NSP 4900, 5,00 x 40 m"},
])
full = repository.resolve(None, "Bentofix NSP 4900")
abbreviated = repository.resolve(None, "Bfix NSP 4900")
self.assertEqual(full, abbreviated)
self.assertEqual(full["status"], "resolved")
def test_secugrid_and_sgrid_have_the_same_family_matching_behavior(self):
repository = ArticleRepository([
{"nr": "04000", "name": "Sgrid 40/40 Q6, 4,75 x 100 m"},
{"nr": "04001", "name": "Sgrid 40/40 Q6, 4,75 x 50 m"},
])
full = repository.resolve(None, "Secugrid 40/40 Q6")
abbreviated = repository.resolve(None, "Sgrid 40/40 Q6")
self.assertEqual(full["status"], "article_ambiguous")
self.assertEqual(full["candidates"], abbreviated["candidates"])
def test_product_family_aliases_are_token_bounded(self):
self.assertEqual(_canonical_name("Secutex R501"), "stex r501")
self.assertEqual(_canonical_name("Disecutex R501"), "disecutex r501")
self.assertEqual(_canonical_name("Bentofixation NSP4900"), "bentofixation nsp 4900")
def test_weak_fuzzy_match_is_not_silently_selected(self):
resolution = self.repository.resolve(None, "Alphx Product")
self.assertEqual(resolution["status"], "article_ambiguous")
self.assertEqual(resolution["reason"], "uncertain_article_match")
self.assertEqual(
resolution["candidates"][0]["article_number"],
"00123",
)
def test_secutex_r501_alias_returns_only_repository_candidates(self):
repository = ArticleRepository.load()
resolution = repository.resolve(None, "Secutex R501")
self.assertEqual(resolution["status"], "article_ambiguous")
candidate_numbers = {
candidate["article_number"] for candidate in resolution["candidates"]
}
self.assertIn("214507", candidate_numbers)
self.assertIn("214508", candidate_numbers)
self.assertTrue(candidate_numbers <= set(repository.by_number))
def test_shared_base_name_returns_repository_candidates(self):
result = self.calculate_fixture({
"article_name_hint": "Shared Product",
"roll_length_m": 20,
"core_diameter_mm": 150,
})
self.assertEqual(result["status"], "needs_clarification")
self.assertEqual(result["field"], "article")
self.assertEqual(result["reason"], "ambiguous_article")
self.assertEqual(result["candidates"], [
{
"article_number": "02000",
"name": "Shared Product, 4,90 x 20 m",
},
{
"article_number": "02001",
"name": "Shared Product, 5,00 x 40 m",
},
])
def test_unknown_conflicting_and_ambiguous_articles_are_structured(self):
unknown = self.calculate_fixture(
{"article_number": "404", "roll_length_m": 20, "core_diameter_mm": 150}
)
conflict = self.calculate_fixture(
{
"article_number": "00123",
"article_name_hint": "Different Product",
"roll_length_m": 20,
"core_diameter_mm": 150,
}
)
ambiguous = self.calculate_fixture(
{
"article_name_hint": "Duplicated Product",
"roll_length_m": 20,
"core_diameter_mm": 150,
}
)
self.assertEqual(unknown["status"], "article_not_found")
self.assertEqual(conflict["status"], "article_conflict")
self.assertEqual(ambiguous["status"], "needs_clarification")
self.assertEqual(ambiguous["ambiguous"], ["article_name_hint"])
def test_duplicate_exact_article_number_is_never_guessed(self):
repository = ArticleRepository(
[
{
"nr": "00123",
"name": "First Record",
"thickness": 2.0,
},
{
"nr": "00123",
"name": "Conflicting Record",
"thickness": 3.0,
},
]
)
result = calculate_roll(
{
"article_number": "00123",
"roll_length_m": 20,
"core_diameter_mm": 150,
},
article_repository=repository,
)
self.assertEqual(result["status"], "needs_clarification")
self.assertEqual(result["ambiguous"], ["article_number"])
self.assertEqual(len(result["article_resolution"]["matches"]), 2)
def test_name_hint_validation_accepts_deterministic_bfix_alias(self):
result = calculate_roll(
{
"article_number": "180205",
"article_name_hint": "Bentofix NSP 4900",
"roll_length_m": 65,
"core_diameter_mm": 150,
}
)
self.assertEqual(result["status"], "success")
def test_search_exact_visible_name_keeps_duplicate_article_numbers(self):
result = ArticleRepository.load().search("Bfix NSP 4900, 5,00 x 40 m")
self.assertEqual(result["status"], "search_results")
self.assertEqual(result["total_matches"], 4)
self.assertEqual(result["candidates"][:2], [
{
"article_number": "180205",
"name": "Bfix NSP 4900, 5,00 x 40 m",
"width_m": 5.0,
"production_site": None,
},
{
"article_number": "8180205",
"name": "Bfix NSP 4900, 5,00 x 40 m",
"width_m": 5.0,
"production_site": "Malaysia",
},
])
def test_article_number_production_site_rule_only_identifies_malaysia(self):
self.assertEqual(article_production_site("8180205"), "Malaysia")
self.assertIsNone(article_production_site("180205"))
def test_search_candidates_expose_site_without_changing_order_or_resolve(self):
repository = ArticleRepository.load()
candidates = repository.search(
"Bfix NSP 4900, 5,00 x 40 m"
)["candidates"]
self.assertEqual(
[candidate["article_number"] for candidate in candidates],
["180205", "8180205", "182815", "206900"],
)
self.assertEqual(
[candidate["production_site"] for candidate in candidates],
[None, "Malaysia", None, None],
)
resolved = repository.resolve("8180205", None)
self.assertEqual(resolved["status"], "resolved")
self.assertNotIn("production_site", resolved["article"])
def test_search_bentofix_alias_matches_bfix_without_resolving_one_article(self):
repository = ArticleRepository.load()
full_name = repository.search("Bentofix NSP 4900")
abbreviated = repository.search("Bfix NSP 4900")
self.assertEqual(full_name["candidates"], abbreviated["candidates"])
self.assertGreater(full_name["total_matches"], 2)
self.assertIn("180205", [
candidate["article_number"] for candidate in full_name["candidates"]
])
def test_search_partial_name_returns_related_products_without_resolving_one(self):
result = ArticleRepository.load().search("NSP 4900")
self.assertEqual(result["status"], "search_results")
self.assertGreater(result["total_matches"], 4)
self.assertIn("180205", [
candidate["article_number"] for candidate in result["candidates"]
])
self.assertIn("206900", [
candidate["article_number"] for candidate in result["candidates"]
])
def test_search_qualifiers_and_dimensions_narrow_real_data_results(self):
repository = ArticleRepository.load()
base = repository.search("Bfix NSP 4900")
dimensions = repository.search("Bfix NSP 4900, 5,00 x 40 m")
qualified = repository.search("Bfix NSP 4900 GRI GCL3")
self.assertLess(dimensions["total_matches"], base["total_matches"])
self.assertEqual(
[candidate["article_number"] for candidate in qualified["candidates"]],
["206900"],
)
def test_search_existing_family_aliases_are_equivalent(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.search("Secutex R 1801")["candidates"],
repository.search("StEx R 1801")["candidates"],
)
self.assertEqual(
repository.search("Secugrid 40/40 Q6")["candidates"],
repository.search("Sgrid 40/40 Q6")["candidates"],
)
def test_search_is_stable_and_handles_empty_and_unknown_queries(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.search("NSP 4900"), repository.search("NSP 4900")
)
self.assertEqual(repository.search(" "), {
"status": "search_not_found",
"query": "",
"total_matches": 0,
"candidates": [],
})
self.assertEqual(
repository.search("not a RollCalc product")["status"],
"search_not_found",
)
def test_search_reports_total_when_the_stable_result_list_is_capped(self):
result = ArticleRepository.load().search("Secugrid")
self.assertEqual(result["status"], "search_results")
self.assertGreater(result["total_matches"], len(result["candidates"]))
self.assertEqual(len(result["candidates"]), 20)
def test_search_does_not_change_existing_resolve_ambiguity(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.resolve(None, "Bentofix NSP 4900")["status"],
"article_ambiguous",
)
def test_name_only_matching_uses_deterministic_bfix_alias(self):
result = self.calculate_fixture({
"article_name_hint": "Bentofix Alias Product",
"roll_length_m": 65,
"core_diameter_mm": 150,
})
self.assertEqual(result["status"], "success")
self.assertEqual(result["resolved_article"]["number"], "03000")
def test_missing_width_requests_clarification_when_not_in_master_data(self):
result = self.calculate_fixture(
{
"article_number": "00999",
"roll_length_m": 20,
"core_diameter_mm": 150,
"include_roll_weight": True,
}
)
self.assertEqual(result["status"], "needs_clarification")
self.assertIn("width_m", result["missing"])
def test_unambiguous_master_width_is_used_with_provenance(self):
result = self.calculate_fixture(
{
"article_number": "00123",
"roll_length_m": 20,
"core_diameter_mm": 150,
"include_roll_weight": True,
}
)
self.assertEqual(result["status"], "success")
self.assertEqual(result["effective_inputs"]["width_m"], {
"value": 4.9,
"source": "article_master_data",
})
self.assertAlmostEqual(result["calculation"]["roll_weight_kg"], 98.0)
def test_invalid_values_and_caller_results_are_rejected(self):
invalid = calculate_roll(
{"roll_length_m": -1, "thickness_mm": 2, "core_diameter_mm": 150}
)
supplied = calculate_roll(
{
"roll_length_m": 10,
"thickness_mm": 2,
"core_diameter_mm": 150,
"diameters": {"average": 1},
}
)
self.assertEqual(invalid["status"], "invalid_parameter")
self.assertEqual(supplied["status"], "invalid_parameter")
self.assertIn("calculated result fields", supplied["invalid"][0]["message"])
self.assertNotIn("request", supplied)
def test_extreme_values_cannot_produce_non_finite_results(self):
result = calculate_roll(
{
"roll_length_m": 1e308,
"thickness_mm": 1e308,
"core_diameter_mm": 150,
}
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertIn("supported calculation range", result["invalid"][0]["message"])
def test_request_result_and_state_are_json_serializable(self):
request = CalculationRequest.from_dict(representative_request())
state = CalculationState(request)
result = calculate_roll(request)
json.dumps(state.to_dict())
json.dumps(result)
class CalculationMutationTests(unittest.TestCase):
def setUp(self):
self.state = CalculationState(
CalculationRequest.from_dict(representative_request())
)
def test_changing_length_recalculates_from_structured_state(self):
result = modify_calculation(self.state, {"roll_length_m": 80})
self.assertEqual(result["status"], "success")
self.assertEqual(result["state"]["request"]["roll_length_m"], 80.0)
self.assertGreater(
result["calculation"]["average_diameter_mm"], 845.8528172323103
)
def test_changing_width_recalculates_weight(self):
result = modify_calculation(self.state, {"width_m": 4.9})
self.assertEqual(result["status"], "success")
self.assertAlmostEqual(result["calculation"]["roll_weight_kg"], 432.96645)
def test_multiple_changes_are_applied_as_one_validated_state(self):
result = modify_calculation(
self.state,
{"roll_length_m": 80, "width_m": 5.0, "include_roll_weight": True},
)
self.assertEqual(result["status"], "success")
request = result["state"]["request"]
self.assertEqual(request["roll_length_m"], 80.0)
self.assertEqual(request["width_m"], 5.0)
self.assertTrue(request["include_roll_weight"])
self.assertIsNotNone(result["calculation"]["roll_weight_kg"])
def test_invalid_multi_change_does_not_mutate_the_existing_state(self):
original = self.state.to_dict()
result = modify_calculation(
self.state,
{"roll_length_m": 80, "unknown_input": 5.0},
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertEqual(self.state.to_dict(), original)
def test_valid_core_change_recalculates(self):
result = modify_calculation(self.state, {"core_type": "194mm steel"})
self.assertEqual(result["status"], "success")
self.assertEqual(result["effective_inputs"]["core_diameter_mm"]["value"], 194.0)
self.assertGreater(
result["calculation"]["average_diameter_mm"], 845.8528172323103
)
def test_ambiguous_or_unknown_core_change_never_guesses(self):
ambiguous = modify_calculation(self.state, {"core_type": "steel"})
unknown = modify_calculation(self.state, {"core_type": "cardboard"})
self.assertEqual(ambiguous["status"], "needs_clarification")
self.assertEqual(unknown["status"], "needs_clarification")
self.assertIn("core_type", ambiguous["ambiguous"])
self.assertIn("core_type", unknown["ambiguous"])
def test_caller_result_fields_are_rejected_from_state(self):
result = modify_calculation(
{
"request": self.state.request.to_dict(),
"calculation": {"average_diameter_mm": 1},
},
{"roll_length_m": 80},
)
self.assertEqual(result["status"], "invalid_parameter")
self.assertIn("unknown state fields", result["invalid"][0]["message"])
if __name__ == "__main__":
unittest.main()