diff --git a/stone/frontend/ir_generator.py b/stone/frontend/ir_generator.py index 74c8d967..d6db2272 100644 --- a/stone/frontend/ir_generator.py +++ b/stone/frontend/ir_generator.py @@ -1704,6 +1704,11 @@ def _filter_namespaces_by_route_whitelist(self): namespace.routes = [] namespace.route_by_name = {} namespace.routes_by_name = {} + + # We need a stable sort in order to keep the resultant output + # the same across runs. + routes.sort() + for route in routes: namespace.add_route(route) diff --git a/stone/ir/api.py b/stone/ir/api.py index cc7fb44e..1b23e182 100644 --- a/stone/ir/api.py +++ b/stone/ir/api.py @@ -416,6 +416,31 @@ def name_with_version(self): def __repr__(self): return 'ApiRoute({})'.format(self.name_with_version()) + def _compare(self, lhs, rhs): + if not isinstance(lhs, ApiRoute): + raise TypeError("Expected ApiRoute for object: {}".format(lhs)) + if not isinstance(rhs, ApiRoute): + raise TypeError("Expected ApiRoute for object: {}".format(rhs)) + + if lhs.name < rhs.name or lhs.version < rhs.version: + return -1 + elif lhs.name > rhs.name or lhs.version > rhs.version: + return 1 + else: + return 0 + + def __lt__(self, other): + return self._compare(self, other) < 0 + def __gt__(self, other): + return self._compare(self, other) > 0 + def __eq__(self, other): + return self._compare(self, other) == 0 + def __le__(self, other): + return self._compare(self, other) <= 0 + def __ge__(self, other): + return self._compare(self, other) >= 0 + def __ne__(self, other): + return self._compare(self, other) != 0 class DeprecationInfo(object): diff --git a/test/test_stone_route_whitelist.py b/test/test_stone_route_whitelist.py index 80570120..733f3d16 100644 --- a/test/test_stone_route_whitelist.py +++ b/test/test_stone_route_whitelist.py @@ -61,6 +61,54 @@ def test_simple(self): self._compare_namespace_names(api, ['test']) self._compare_datatype_names(api.namespaces['test'], ['TestArg2', 'TestResult2']) + def test_stable_ordering(self): + """ + Tests that route generation returns the same route order on generation. + """ + text = textwrap.dedent("""\ + namespace test + + struct TestArg + f String + "test doc" + example default + f = "asdf" + struct TestResult + f String + "test doc" + example default + f = "asdf" + route TestRoute (TestArg, TestResult, Void) + "test doc" + + route OtherRoute (Void, Void, Void) + "Additional Route" + + """) + + route_whitelist = ["OtherRoute", "TestRoute"] + + for x in range(2, 100): + text += textwrap.dedent("""\ + route TestRoute:{} (TestArg, TestResult, Void) + "test doc" + """.format(x)) + route_whitelist.append("TestRoute:{}".format(x)) + + route_whitelist_filter = { + "route_whitelist": {"test": route_whitelist}, + "datatype_whitelist": {} + } + + api = specs_to_ir([('test.stone', text)], route_whitelist_filter=route_whitelist_filter) + routes = api.namespaces['test'].routes + self.assertEqual(len(routes), 100) + self.assertEqual(routes[0].name, "OtherRoute") + + for x in range(1, 100): + self.assertEqual(routes[x].name, "TestRoute") + self.assertEqual(routes[x].version, x) + def test_star(self): """ Tests that inputs with "*" work as expected.