Skip to content

Commit

Permalink
Add tests for rosdep checks (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
cottsay authored Sep 21, 2024
1 parent 7b81baf commit 669a5e3
Show file tree
Hide file tree
Showing 2 changed files with 361 additions and 0 deletions.
6 changes: 6 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
addfinalizer
apache
archlinux
bubblify
colcon
committish
Expand All @@ -10,17 +11,22 @@ deserialized
diffs
eoan
filepath
fixturenames
fred
gentoo
github
https
india
iterdir
itertools
jessie
juliet
lenny
libdelta
linter
linting
mantic
metafunc
mktemp
mypy
namedtuple
Expand Down
355 changes: 355 additions & 0 deletions test/test_rosdep_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
# Copyright 2024 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

import itertools
from pathlib import Path
from typing import Iterable

from git import Repo
import pytest
from rosdistro_reviewer.element_analyzer.rosdep import RosdepAnalyzer
from rosdistro_reviewer.review import Recommendation
import yaml


@pytest.fixture
def rosdep_repo(empty_repo) -> Iterable[Repo]:
repo_dir = Path(empty_repo.working_tree_dir)
(repo_dir / 'rosdep').mkdir()

for file_name, data in EXISTING_RULES.items():
file_path = repo_dir / 'rosdep' / file_name
with file_path.open('w') as f:
yaml.dump(data, f)

empty_repo.index.add(str(file_path))

empty_repo.index.commit('Add rosdep files')

return empty_repo


def test_no_files(empty_repo):
repo_dir = Path(empty_repo.working_tree_dir)
extension = RosdepAnalyzer()
assert (None, None) == extension.analyze(repo_dir)


def test_no_changes(rosdep_repo):
repo_dir = Path(rosdep_repo.working_tree_dir)
extension = RosdepAnalyzer()
assert (None, None) == extension.analyze(repo_dir)


def _all_combinations(iterable):
for r in range(1, 3):
yield from itertools.combinations(iterable, r)


def _merge_two_rules(first, second):
if isinstance(first, dict):
assert isinstance(second, dict)
result = dict(first)
for k, v in second.items():
if k not in result:
result[k] = v
else:
result[k] = _merge_two_rules(result[k], v)
return result
elif isinstance(first, list):
assert isinstance(second, list)
result = []
for v in first:
try:
i = second.index(v)
except ValueError:
result.append(v)
else:
result.append(_merge_two_rules(v, second[i]))
for v in second:
if v not in result:
result.append(v)
return result
else:
assert first == second
return first


def _merge_all_rules(rules_list):
result = {}
for rules in rules_list:
result = _merge_two_rules(result, rules)
return result


# These rules are already committed to the db and aren't part of the change.
# They must be syntactically correct, but do not necessarily need to pass
# the checks.
EXISTING_RULES = {
'base.yaml': {
'existing-golf': {
'fedora': ['existing-golf'],
},
'existing-hotel': {
'ubuntu': {
'*': {
'apt': {
'packages': ['existing-hotel'],
},
},
},
},
},
'python.yaml': {
'python.yaml': {
'python3-existing-india': {
'fedora': ['python3-existing-india'],
},
},
},
}


# These are the "control" rules for rosdep check validation.
# Each one must pass all checks and be syntactically correct.
CONTROL_RULES = {
'base.yaml': {
'control-alpha': {
'debian': {
'apt': {
'packages': ['control-alpha'],
},
},
'ubuntu': {
'*': {
'apt': {
'packages': ['control-alpha'],
},
},
},
},
},
'python.yaml': {
'python3-control-bravo': {
'*': {
'pip': {
'packages': ['python3-control-bravo'],
},
},
'ubuntu': ['python3-control-bravo'],
},
'python3-control-bravo-pip': {
'*': {
'pip': {
'packages': ['python3-control-bravo'],
},
},
},
'python3-control-charlie': {
'fedora': ['python3-control-charlie'],
'ubuntu': None,
},
},
}


# This is a list of violations to check for.
# To avoid collisions, each check should use unique package names.
VIOLATIONS = {
'*': CONTROL_RULES,
'A': {
# This key should end in -pip
'python.yaml': {
'python3-alpha': {
'*': {
'pip': {
'packages': ['alpha'],
},
},
},
},
},
'B': {
# This key should not end in -pip
'python.yaml': {
'python3-bravo-pip': {
'fedora': ['python3-bravo'],
},
},
},
'C': {
# This key belongs in python.yaml
'base.yaml': {
'python3-charlie': {
'ubuntu': ['python3-charlie'],
},
},
},
'D': {
# This key name should match the ubuntu package name
'base.yaml': {
'delta': {
'ubuntu': ['libdelta'],
},
},
},
'E': {
# This key name should match the ubuntu package name
'base.yaml': {
'echo': {
'ubuntu': {
'*': {
'apt': {
'packages': ['libdelta'],
},
},
},
},
},
},
'F': {
# This key is defined in multiple files
'base.yaml': {
'foxtrot': {
'ubuntu': ['foxtrot'],
},
},
'python.yaml': {
'foxtrot': {
'*': {
'pip': {
'packages': ['foxtrot'],
},
},
},
},
},
'G': {
# This is a rule for an unsupported OS
'base.yaml': {
'existing-golf': {
'archlinux': ['golf'],
},
},
},
'H': {
# This is a rule for an unsupported version of Ubuntu
'base.yaml': {
'existing-hotel': {
'ubuntu': {
'xenial': ['hotel'],
},
},
},
},
'I': {
# The pip installer is not supported on Gentoo
'python.yaml': {
'python3-existing-india': {
'gentoo': {
'pip': {
'packages': ['india'],
},
},
},
},
},
'J': {
# The pip installer is not supported on Gentoo
'python.yaml': {
'python3-juliet-pip': {
'gentoo': {
'*': {
'pip': {
'packages': ['juliet'],
},
},
},
},
},
},
}


def pytest_generate_tests(metafunc):
if 'violation_rules' not in metafunc.fixturenames:
return
combinations = {
''.join(key_set): _merge_all_rules(map(VIOLATIONS.get, key_set))
for key_set in _all_combinations(sorted(VIOLATIONS.keys()))
if key_set != ('*',)
}
metafunc.parametrize(
'violation_rules',
combinations.values(),
ids=combinations.keys())


def test_control(rosdep_repo):
repo_dir = Path(rosdep_repo.working_tree_dir)
extension = RosdepAnalyzer()

rules = _merge_two_rules(EXISTING_RULES, CONTROL_RULES)
for file_name, data in rules.items():
file_path = repo_dir / 'rosdep' / file_name
with file_path.open('w') as f:
yaml.dump(data, f)

criteria, annotations = extension.analyze(repo_dir)
assert criteria and not annotations
assert all(Recommendation.APPROVE == c.recommendation for c in criteria)


def test_target_ref(rosdep_repo):
repo_dir = Path(rosdep_repo.working_tree_dir)
extension = RosdepAnalyzer()

rules = _merge_two_rules(EXISTING_RULES, CONTROL_RULES)
for file_name, data in rules.items():
file_path = repo_dir / 'rosdep' / file_name
with file_path.open('w') as f:
yaml.dump(data, f)

rosdep_repo.index.add(str(file_path))

rosdep_repo.index.commit('Add control rules')

# Add some violations to the stage, choose set 'A' as candidate
rules = _merge_two_rules(rules, VIOLATIONS['A'])
for file_name, data in rules.items():
file_path = repo_dir / 'rosdep' / file_name
with file_path.open('w') as f:
yaml.dump(data, f)

criteria, annotations = extension.analyze(repo_dir, head_ref='HEAD')
assert criteria and not annotations
assert all(Recommendation.APPROVE == c.recommendation for c in criteria)

criteria, annotations = extension.analyze(repo_dir)
assert criteria and annotations
assert any(Recommendation.APPROVE != c.recommendation for c in criteria)


def test_removal_only(rosdep_repo):
repo_dir = Path(rosdep_repo.working_tree_dir)
extension = RosdepAnalyzer()

for file_name in EXISTING_RULES.keys():
(repo_dir / 'rosdep' / file_name).write_text('')

assert (None, None) == extension.analyze(repo_dir)


def test_violation(rosdep_repo, violation_rules):
repo_dir = Path(rosdep_repo.working_tree_dir)
extension = RosdepAnalyzer()

rules = _merge_two_rules(EXISTING_RULES, violation_rules)
for file_name, data in rules.items():
file_path = repo_dir / 'rosdep' / file_name
with file_path.open('w') as f:
yaml.dump(data, f)

criteria, annotations = extension.analyze(repo_dir)
assert criteria and annotations
assert any(Recommendation.APPROVE != c.recommendation for c in criteria)

0 comments on commit 669a5e3

Please sign in to comment.