metadata: define a clear DependencyMetadata interface
This CL adds a typed interface that exposes parsed metadata for downstream consumption. Conventionally: - A validated field should be retrieved by the property of the same name - A validated field returns "None" if said field is not provided, or is clearly invalid (e.g. "Unknown" values) - Raw values can still be retrieved with get_entries() When using the properties accessor, fields are normalized and/or coerced to a suitable type (e.g. list of str, str of a particular format). Bug: b/321154076 Change-Id: Ia56969a838e682a7b7eb1dc0781d48e1e38a2ff0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/5446637 Reviewed-by: Rachael Newitt <renewitt@google.com> Commit-Queue: Jiewei Qian <qjw@chromium.org>changes/37/5446637/10
parent
1a61eb625d
commit
b7ed76a09d
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2024 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
# The repo's root directory.
|
||||
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", "..", ".."))
|
||||
|
||||
# Add the repo's root directory for clearer imports.
|
||||
sys.path.insert(0, _ROOT_DIR)
|
||||
|
||||
import metadata.fields.field_types as field_types
|
||||
import metadata.fields.custom.version as version_field
|
||||
import metadata.fields.util as util
|
||||
import metadata.validation_result as vr
|
||||
|
||||
|
||||
class RevisionField(field_types.SingleLineTextField):
|
||||
"""Custom field for the revision."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name="Revision")
|
||||
|
||||
def narrow_type(self, value: str) -> Optional[str]:
|
||||
value = super().narrow_type(value)
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if version_field.version_is_unknown(value):
|
||||
return None
|
||||
|
||||
if util.is_known_invalid_value(value):
|
||||
return None
|
||||
|
||||
return value
|
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2024 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from typing import Any, Callable
|
||||
|
||||
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
# The repo's root directory.
|
||||
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
|
||||
|
||||
# Add the repo's root directory for clearer imports.
|
||||
sys.path.insert(0, _ROOT_DIR)
|
||||
|
||||
from metadata.fields.field_types import MetadataField
|
||||
import metadata.fields.known as fields
|
||||
from metadata.dependency_metadata import DependencyMetadata
|
||||
|
||||
|
||||
class FieldValidationTest(unittest.TestCase):
|
||||
"""Tests narrow_type() on fields we validate and extract structural data."""
|
||||
|
||||
def _test_on_field(self, field: MetadataField) -> Callable:
|
||||
|
||||
def expect(value: str, expected_value: Any, reason: str):
|
||||
output = field.narrow_type(value)
|
||||
self.assertEqual(
|
||||
output, expected_value,
|
||||
f'Field "{field.get_name()}" should {reason}. Input value'
|
||||
f' was: "{value}", but got coerced into {repr(output)}')
|
||||
|
||||
return expect
|
||||
|
||||
def test_name(self):
|
||||
expect = self._test_on_field(fields.NAME)
|
||||
expect("package name", "package name", "return as-is")
|
||||
expect("", "", "not coerce empty string to `None`")
|
||||
|
||||
def test_short_name(self):
|
||||
expect = self._test_on_field(fields.SHORT_NAME)
|
||||
expect("pkg-name", "pkg-name", "return as-is")
|
||||
expect("", "", "not coerce empty string to `None`")
|
||||
|
||||
def test_url(self):
|
||||
expect = self._test_on_field(fields.URL)
|
||||
expect("", None, "treat empty string as None")
|
||||
expect("https://example.com/", ["https://example.com/"],
|
||||
"return valid url")
|
||||
expect("https://example.com/,\nhttps://example2.com/",
|
||||
["https://example.com/", "https://example2.com/"],
|
||||
"return multiple valid urls")
|
||||
expect("file://test", [], "reject unsupported scheme")
|
||||
expect("file://test,\nhttps://example.com", ["https://example.com"],
|
||||
"reject unsupported scheme")
|
||||
expect("HTTPS://example.com", ["https://example.com"],
|
||||
"canonicalize url")
|
||||
expect("http", [], "reject invalid url")
|
||||
expect("This is the canonical repo.", None,
|
||||
"understand the this repo is canonical message")
|
||||
|
||||
def test_version(self):
|
||||
expect = self._test_on_field(fields.VERSION)
|
||||
expect("", None, "treat empty string as None")
|
||||
expect("0", None, "treat invalid value as None")
|
||||
expect("varies", None, "treat invalid value as None")
|
||||
expect("see deps", None, "treat invalid value as None")
|
||||
expect("N/A", None, "N/A is treated as None")
|
||||
expect("Not applicable.", None, "N/A is treated as None")
|
||||
|
||||
def test_date(self):
|
||||
expect = self._test_on_field(fields.DATE)
|
||||
expect("", None, "treat empty string as None")
|
||||
expect("0", None, "treat invalid value as None")
|
||||
expect("varies", None, "treat invalid value as None")
|
||||
expect("2024-01-02", "2024-01-02", "accepts ISO 8601 date")
|
||||
expect("2024-01-02T03:04:05Z", "2024-01-02",
|
||||
"accepts ISO 8601 date time")
|
||||
expect("Jan 2 2024", "2024-01-02", "accepts locale format")
|
||||
expect(
|
||||
"02/03/2000", "2000-03-02",
|
||||
"accepts ambiguous MM/DD format (better than no date info at all)")
|
||||
expect("11/30/2000", "2000-11-30", "accepts unambiguous MM/DD format")
|
||||
|
||||
def test_revision(self):
|
||||
expect = self._test_on_field(fields.REVISION)
|
||||
expect("", None, "treat empty string as None")
|
||||
expect("0", None, "treat invalid value as None")
|
||||
expect("varies", None, "treat invalid value as None")
|
||||
expect("see deps", None, "treat invalid value as None")
|
||||
expect("N/A", None, "N/A is treated as None")
|
||||
expect("Not applicable.", None, "N/A is treated as None")
|
||||
|
||||
def test_license(self):
|
||||
expect = self._test_on_field(fields.LICENSE)
|
||||
expect("", None, "treat empty string as None")
|
||||
expect("LICENSE-1", ["LICENSE-1"], "return as a list")
|
||||
expect("LGPL v2 and BSD", ["LGPL v2", "BSD"], "return as a list")
|
||||
|
||||
def test_license_file(self):
|
||||
# TODO(b/321154076): Consider excluding files that doesn't exist on
|
||||
# disk if it's not too hard.
|
||||
#
|
||||
# Right now, we return the unparsed license file field as-is.
|
||||
expect = self._test_on_field(fields.LICENSE_FILE)
|
||||
expect("src/file", "src/file", "return value as-is")
|
||||
|
||||
def test_security_critical(self):
|
||||
expect = self._test_on_field(fields.SECURITY_CRITICAL)
|
||||
expect("yes", True, "understand truthy value")
|
||||
expect("Yes", True, "understand truthy value")
|
||||
expect("no", False, "understand falsey value")
|
||||
expect("No, because", False,
|
||||
"understand falsey value, with description")
|
||||
|
||||
def test_shipped(self):
|
||||
expect = self._test_on_field(fields.SHIPPED)
|
||||
expect("yes", True, "understand truthy value")
|
||||
expect("Yes, but", True, "understand truthy value with extra comment")
|
||||
expect("no", False, "understand falsey value")
|
||||
expect("no, because", False,
|
||||
"understand falsey value, with extra comment")
|
||||
|
||||
def test_shipped_in_chromium(self):
|
||||
expect = self._test_on_field(fields.SHIPPED_IN_CHROMIUM)
|
||||
expect("yes", True, "understand truthy value")
|
||||
expect("Yes", True, "understand truthy value")
|
||||
expect("no", False, "understand falsey value")
|
||||
expect("no, because", False,
|
||||
"understand falsey value, with extra comment")
|
||||
|
||||
def test_license_android_compatible(self):
|
||||
expect = self._test_on_field(fields.LICENSE_ANDROID_COMPATIBLE)
|
||||
expect("yes", True, "understand truthy value")
|
||||
expect("Yes", True, "understand truthy value")
|
||||
expect("no", False, "understand falsey value")
|
||||
expect("no, because", False,
|
||||
"understand falsey value, with extra comment")
|
||||
|
||||
def test_cpe_prefix(self):
|
||||
expect = self._test_on_field(fields.CPE_PREFIX)
|
||||
expect("unknown", "unknown", "understand unknown")
|
||||
expect("bad_cpe_format", None, "rejects invalid value")
|
||||
expect("cpe:/a:d3", "cpe:/a:d3", "accept a valid cpe prefix")
|
||||
expect("cpe:/a:D3", "cpe:/a:d3", "normalize to lowercase")
|
||||
|
||||
def test_description(self):
|
||||
expect = self._test_on_field(fields.DESCRIPTION)
|
||||
expect("desc", "desc", "return value as-is")
|
||||
|
||||
def test_local_modification(self):
|
||||
expect = self._test_on_field(fields.LOCAL_MODIFICATIONS)
|
||||
expect("none", False, "understands none")
|
||||
expect("(none)", False, "understands none")
|
||||
expect("not applicable", False, "understands N/A")
|
||||
expect("", False, "treat empty string as False")
|
||||
expect("modified X file", "modified X file",
|
||||
"return value as-is if it doesn't mean no modification")
|
||||
|
||||
def test_dependency_data_return_as_property(self):
|
||||
dm = DependencyMetadata()
|
||||
dm.add_entry("name", "package")
|
||||
dm.add_entry("url", "git://git@example.com,\nbad_url://example.com")
|
||||
dm.add_entry("security critical", "no")
|
||||
dm.add_entry("date", "2024-01-02")
|
||||
dm.add_entry("revision", "")
|
||||
|
||||
self.assertEqual(dm.name, "package")
|
||||
self.assertEqual(dm.url, ["git://git@example.com"])
|
||||
self.assertEqual(dm.security_critical, False)
|
||||
self.assertEqual(dm.date, "2024-01-02")
|
||||
self.assertEqual(dm.revision, None)
|
||||
self.assertEqual(dm.version, None)
|
||||
|
||||
def test_dependency_data_repo_is_canonical(self):
|
||||
dm = DependencyMetadata()
|
||||
dm.add_entry("name", "package")
|
||||
dm.add_entry("url", "This is the canonical repo.")
|
||||
|
||||
self.assertEqual(dm.url, None)
|
||||
self.assertEqual(dm.is_canonical, True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
Reference in New Issue