diff --git a/doc/source/_exts/candidates.py b/doc/source/_exts/candidates.py index 5dae851a..a07db1e1 100644 --- a/doc/source/_exts/candidates.py +++ b/doc/source/_exts/candidates.py @@ -23,6 +23,7 @@ import jinja2.environment from sphinx.util import logging from sphinx.util.nodes import nested_parse_with_titles +from openstack_election import series_sorting from openstack_election import utils LOG = logging.getLogger(__name__) @@ -110,7 +111,9 @@ def build_lists(app): "" ] archived_dir = os.path.join(".", "doc", "source", "results") - for previous in sorted(os.listdir(archived_dir), reverse=True): + dirs = sorted(os.listdir(archived_dir), key=series_sorting.keyfunc, + reverse=True) + for previous in dirs: if build_archive(previous, "ptl"): previous_toc.append(" results/%s/ptl.rst" % previous) if build_archive(previous, "tc"): diff --git a/openstack_election/series_sorting.py b/openstack_election/series_sorting.py new file mode 100644 index 00000000..444cf4dc --- /dev/null +++ b/openstack_election/series_sorting.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +_series_data = [ + ["austin", "bexar", "cactus", "diablo", "essex", "folsom", "grizzly", + "havana", "icehouse", "juno", "kilo", "liberty", "mitaka", "newton", + "ocata", "pike", "queens", "rocky", "stein", "train", "ussuri", + "victoria", "wallaby", "xena", "yoga", "zed"], + # ["antelope", bobcat, etc etc etc],` +] + + +def keyfunc(series_name): + assert series_name.isascii() + + # NOTE(tonyb): Create a private copy to avoid mutating input variable + _series_name = series_name.lower() + # This for/else statement looks for a series_name in series_data. If it + # is found stop looking (via break), because we have all the information + # we need. If the series name isn't found, i.e a run through the entire + # series_data list-of-lists, the 'else' clause will be executed to do our + # best to deduce the sort key from there. + for series_nr, series_names in enumerate(_series_data): + if _series_name in series_names: + series_idx = series_names.index(_series_name) + break + else: + if _series_name[0].isalpha(): + series_nr += 1 + series_idx = ord(_series_name[0]) - ord("a") + elif _series_name[0].isdigit(): + (year, release) = map(int, _series_name.split(".")) + # This arithmetic comes from the fact that we started using + # year.release naming scheme, after we completed a full list + # of the alphabet. + # This happened with the 2023.1 release. To date it's two + # releases per year. If that changes this code will need to + # be updated. + # Releases "austin" -> "zed" are 0 -> 25 so 2023.1 is the 26th + # OpenStack release + (series_nr, series_idx) = \ + divmod(26 + ((year - 2023) * 2 + (release - 1)), 26) + else: + assert False + # TODO(tonyb): Do we want to switch this to aa_austin, ba_2023.1 to force + # a stable sort order + return series_nr * 26 + series_idx diff --git a/openstack_election/tests/test_sorting.py b/openstack_election/tests/test_sorting.py new file mode 100644 index 00000000..82b00484 --- /dev/null +++ b/openstack_election/tests/test_sorting.py @@ -0,0 +1,70 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from unittest import mock + +from openstack_election import series_sorting +from openstack_election.tests import base + + +class TestSeries_Sorting(base.ElectionTestCase): + FAKE_SERIES_DATA = [ + series_sorting._series_data[0], + ["antelope", "bobcat", "camel", "duck", "elephant", "fox", "gorilla", + "hamster", "ibex", "jellyfish", "kudu", "lion", "meerkat", "narwhal", + "otter", "pony", "quail", "raccoon", "salmon", "termite", "uakari", + "viperfish", "whale", "xerus", "yak", "zebra"] + ] + + def test_assert_nonascii(self): + self.assertRaises(AssertionError, series_sorting.keyfunc, "ハロー") + + def test_assert_nonaplphanumeric(self): + self.assertRaises(AssertionError, series_sorting.keyfunc, "__austin") + + def test_simple(self): + test_series_list = ["antelope", "victoria", "2023.1", "zed", + "c-release-not-cactus"] + sorted_series_list = ["victoria", "zed", "antelope", "2023.1", + "c-release-not-cactus"] + self.assertEqual(sorted(test_series_list, key=series_sorting.keyfunc), + sorted_series_list) + + def test_simple_with_case(self): + test_series_list = ["Antelope", "victoria", "2023.1", "zed", + "C-release-not-cactus"] + sorted_series_list = ["victoria", "zed", "Antelope", "2023.1", + "C-release-not-cactus"] + self.assertEqual(sorted(test_series_list, key=series_sorting.keyfunc), + sorted_series_list) + + @mock.patch.object(series_sorting, "_series_data", FAKE_SERIES_DATA) + def test_with_series_2(self): + test_series_list = ["antelope", "austin", "aardvark"] + sorted_series_list = ["austin", "antelope", "aardvark"] + self.assertEqual(sorted(test_series_list, key=series_sorting.keyfunc), + sorted_series_list) + + @mock.patch.object(series_sorting, "_series_data", FAKE_SERIES_DATA) + def test_with_series_2_rollover(self): + test_series_list = ["antelope", "austin", "zebra", "yak", "aardvark"] + sorted_series_list = ["austin", "antelope", "yak", "zebra", "aardvark"] + self.assertEqual(sorted(test_series_list, key=series_sorting.keyfunc), + sorted_series_list) + + @mock.patch.object(series_sorting, "_series_data", FAKE_SERIES_DATA) + def test_with_series_2_mixed_styles(self): + test_series_list = ["elephant", "2023.1", "duck", "2024.2", + "aardvark", "2024.1"] + sorted_series_list = ["2023.1", "2024.1", "duck", "2024.2", + "elephant", "aardvark"] + self.assertEqual(sorted(test_series_list, key=series_sorting.keyfunc), + sorted_series_list)