Skip to content

Commit cb3ec26

Browse files
committed
Fix: PURL version sorting with univers library and validation
Signed-off-by: Rejwanul Hoque <hoquerejwanulrh@gmail.com>
1 parent e7f441f commit cb3ec26

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

packagedb/tests/test_models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from packagedb.models import PackageWatch
2121
from packagedb.models import Party
2222
from packagedb.models import Resource
23+
from packagedb.models import sort_version
2324

2425

2526
class ResourceModelTestCase(TransactionTestCase):
@@ -494,3 +495,70 @@ def test_get_or_none(self):
494495
package = Package.objects.filter(download_url="http://a.ab").get_or_none()
495496
assert package
496497
assert Package.objects.filter(download_url="http://a.ab-foobar").get_or_none() is None
498+
499+
500+
class SortVersionTestCase(TransactionTestCase):
501+
"""Comprehensive tests for the sort_version function."""
502+
503+
def tearDown(self):
504+
Package.objects.all().delete()
505+
506+
def _create_packages(self, pkg_type, versions, **kwargs):
507+
"""Helper to create packages with given versions."""
508+
return [
509+
Package.objects.create(
510+
download_url=f"http://{pkg_type}-{hash(version)}.com",
511+
type=pkg_type,
512+
version=version,
513+
**kwargs
514+
)
515+
for version in versions
516+
]
517+
518+
def test_sort_version_empty_list(self):
519+
"""Test sorting an empty list."""
520+
self.assertEqual([], sort_version([]))
521+
522+
def test_ecosystem_versions(self):
523+
"""Test version sorting across multiple package ecosystems."""
524+
test_cases = [
525+
("npm", ["1.10.0", "2.0.0", "1.0.0", "1.2.0"], ["1.0.0", "1.2.0", "1.10.0", "2.0.0"], {"name": "lodash"}),
526+
("pypi", ["1.0.1", "1.0rc1", "1.0", "1.0a1", "1.0b1"], ["1.0a1", "1.0b1", "1.0rc1", "1.0", "1.0.1"], {"name": "django"}),
527+
("maven", ["4.10", "4.0", "4.2"], ["4.0", "4.2", "4.10"], {"namespace": "junit", "name": "junit"}),
528+
("swift", ["2.0.0", "1.1.5", "1.0.0", "1.1.5^{}"], ["1.0.0", "1.1.5", "1.1.5^{}", "2.0.0"], {"name": "alamofire"}),
529+
("gem", ["4.0.0", "3.0.0", "3.2.0"], ["3.0.0", "3.2.0", "4.0.0"], {"name": "rails"}),
530+
("deb", ["1.0-10", "1.0-1", "1.0-2"], ["1.0-1", "1.0-2", "1.0-10"], {"name": "deb-pkg"}),
531+
("nuget", ["11.0.0", "10.0.0", "9.0.0"], ["9.0.0", "10.0.0", "11.0.0"], {"name": "Newtonsoft.Json"}),
532+
("generic", ["1.10", "1.0", "1.2"], ["1.0", "1.2", "1.10"], {"name": "gen-pkg"}),
533+
("cargo", ["1.0.100", "1.0.0", "1.0.20"], ["1.0.0", "1.0.20", "1.0.100"], {"name": "serde"}),
534+
("composer", ["4.0.0", "3.0.0", "3.1.0"], ["3.0.0", "3.1.0", "4.0.0"], {"name": "sf-console"}),
535+
("golang", ["v0.9.1", "v0.8.0", "v0.9.0"], ["v0.8.0", "v0.9.0", "v0.9.1"], {"namespace": "github.com/pkg", "name": "errors"}),
536+
("rpm", ["3.10.0-10", "3.10.0-1", "3.10.0-2"], ["3.10.0-1", "3.10.0-2", "3.10.0-10"], {"name": "kernel"}),
537+
("unknown-type", ["1.10", "1.0", "1.2"], ["1.0", "1.2", "1.10"], {"name": "unk-pkg"}),
538+
("npm", ["invalid-10", "invalid-1", "invalid-2"], ["invalid-1", "invalid-2", "invalid-10"], {"name": "inv-test"}),
539+
]
540+
541+
for pkg_type, unsorted, expected, kwargs in test_cases:
542+
with self.subTest(pkg_type=pkg_type):
543+
packages = self._create_packages(pkg_type, unsorted, **kwargs)
544+
sorted_versions = [p.version for p in sort_version(packages)]
545+
self.assertEqual(expected, sorted_versions, f"Failed for {pkg_type}")
546+
547+
def test_sort_version_generator_input(self):
548+
"""Test with generator input."""
549+
packages = self._create_packages("npm", ["1.10.0", "1.0.0", "1.5.0"], name="gen-pkg")
550+
gen = (p for p in packages)
551+
sorted_packages = sort_version(gen)
552+
self.assertEqual(3, len(sorted_packages))
553+
554+
def test_sort_version_explicit_type(self):
555+
"""Test with explicit package_type parameter."""
556+
packages = self._create_packages("npm", ["1.10.0", "1.2.0", "1.0.0"], name="exp-pkg")
557+
sorted_packages = sort_version(packages, package_type="npm")
558+
self.assertEqual("1.0.0", sorted_packages[0].version)
559+
560+
def test_get_latest_version_integration(self):
561+
"""Test get_latest_version uses sort_version correctly."""
562+
packages = self._create_packages("npm", ["1.0.0", "1.10.0", "1.2.0"], name="test-pkg")
563+
latest = packages[0].get_latest_version()
564+
self.assertEqual("1.10.0", latest.version)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# purldb is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/purldb for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from django.test import TransactionTestCase
11+
12+
from packagedb.models import Package
13+
from packagedb.models import sort_version
14+
15+
16+
class SortVersionIntegrationTestCase(TransactionTestCase):
17+
"""Integration tests for sort_version with real-world PURL data."""
18+
19+
def tearDown(self):
20+
Package.objects.all().delete()
21+
22+
def _test_ecosystem_sorting(self, pkg_type, versions_unordered, expected_ordered, **kwargs):
23+
"""Helper to test version sorting for any ecosystem."""
24+
packages = [
25+
Package.objects.create(
26+
download_url=f"http://{pkg_type}-{hash(version)}.com",
27+
type=pkg_type,
28+
version=version,
29+
**kwargs
30+
)
31+
for version in versions_unordered
32+
]
33+
sorted_versions = [p.version for p in sort_version(packages)]
34+
self.assertEqual(expected_ordered, sorted_versions)
35+
36+
def test_ecosystem_versions(self):
37+
"""Test version sorting for multiple real-world ecosystems."""
38+
test_cases = [
39+
("npm", ["4.17.21", "4.17.20", "4.17.10", "4.17.4", "4.16.6", "4.0.0", "3.10.1", "1.3.1"],
40+
["1.3.1", "3.10.1", "4.0.0", "4.16.6", "4.17.4", "4.17.10", "4.17.20", "4.17.21"], {"name": "lodash"}),
41+
("pypi", ["4.1", "4.1rc1", "4.1b1", "4.1a1", "4.0.8", "4.0", "3.2.16", "2.1.15"],
42+
["2.1.15", "3.2.16", "4.0", "4.0.8", "4.1a1", "4.1b1", "4.1rc1", "4.1"], {"name": "django"}),
43+
("maven", ["4.13.2", "4.13", "4.10", "4.8.2", "4.5", "3.8.2", "3.8.1"],
44+
["3.8.1", "3.8.2", "4.5", "4.8.2", "4.10", "4.13", "4.13.2"], {"namespace": "junit", "name": "junit"}),
45+
("gem", ["7.0.4", "7.0.3.1", "6.1.6.1", "6.0.6", "5.2.8.1", "5.2.0"],
46+
["5.2.0", "5.2.8.1", "6.0.6", "6.1.6.1", "7.0.3.1", "7.0.4"], {"name": "rails"}),
47+
("nuget", ["13.0.1", "12.0.3", "10.0.3", "9.0.1", "8.0.3", "6.0.8"],
48+
["6.0.8", "8.0.3", "9.0.1", "10.0.3", "12.0.3", "13.0.1"], {"name": "Newtonsoft.Json"}),
49+
("cargo", ["1.0.147", "1.0.100", "1.0.10", "1.0.0", "0.9.15", "0.9.0"],
50+
["0.9.0", "0.9.15", "1.0.0", "1.0.10", "1.0.100", "1.0.147"], {"name": "serde"}),
51+
("deb", ["2.31-13+deb11u5", "2.31-13", "2.28-10", "2.27-3ubuntu1", "2.24-11+deb9u4"],
52+
["2.24-11+deb9u4", "2.27-3ubuntu1", "2.28-10", "2.31-13", "2.31-13+deb11u5"], {"name": "libc6"}),
53+
("golang", ["v1.8.1", "v1.7.0", "v1.5.0", "v1.2.0", "v1.0.0", "v0.9.0"],
54+
["v0.9.0", "v1.0.0", "v1.2.0", "v1.5.0", "v1.7.0", "v1.8.1"], {"namespace": "github.com/pkg", "name": "errors"}),
55+
]
56+
for pkg_type, unsorted, expected, kwargs in test_cases:
57+
with self.subTest(pkg_type=pkg_type):
58+
self._test_ecosystem_sorting(pkg_type, unsorted, expected, **kwargs)
59+
60+
def test_swift_with_git_tag_suffix(self):
61+
"""
62+
Test Swift packages with Git tag suffixes (issue #808).
63+
64+
Swift is unsupported by univers, so uses natsort fallback.
65+
Versions with ^{} suffix should come after their base versions.
66+
"""
67+
versions = ["5.6.4", "5.6.4^{}", "5.4.4", "5.4.4^{}", "5.2.2", "5.2.2^{}", "4.8.2"]
68+
packages = [
69+
Package.objects.create(
70+
download_url=f"http://swift-{i}.com",
71+
type="swift",
72+
name="Alamofire",
73+
version=version
74+
)
75+
for i, version in enumerate(versions)
76+
]
77+
sorted_versions = [p.version for p in sort_version(packages)]
78+
79+
# Base versions should come before their ^{} suffixed versions
80+
self.assertLess(
81+
sorted_versions.index("5.2.2"),
82+
sorted_versions.index("5.2.2^{}")
83+
)
84+
self.assertLess(
85+
sorted_versions.index("5.4.4"),
86+
sorted_versions.index("5.4.4^{}")
87+
)
88+
89+
def test_cross_ecosystem_latest_version(self):
90+
"""Test get_latest_version across different ecosystems."""
91+
# npm
92+
npm_pkgs = [
93+
Package.objects.create(download_url=f"http://npm-{i}.com", type="npm", name="test", version=v)
94+
for i, v in enumerate(["1.0.0", "1.10.0", "1.2.0"])
95+
]
96+
self.assertEqual(npm_pkgs[1], npm_pkgs[0].get_latest_version())
97+
98+
# pypi
99+
pypi_pkgs = [
100+
Package.objects.create(download_url=f"http://pypi-{i}.com", type="pypi", name="pkg", version=v)
101+
for i, v in enumerate(["2.0", "2.0.1", "2.0a1"])
102+
]
103+
self.assertEqual(pypi_pkgs[1], pypi_pkgs[0].get_latest_version())

0 commit comments

Comments
 (0)