rspec/rspec-tools/tests/test_coverage.py

190 lines
8.7 KiB
Python

import os
import shutil
import pytest
from git import Repo
from pathlib import Path
from datetime import datetime
from unittest.mock import (patch, PropertyMock)
from contextlib import contextmanager
from rspec_tools.coverage import (update_coverage_for_all_repos,
update_coverage_for_repo,
update_coverage_for_repo_version)
from rspec_tools.utils import (load_json, pushd)
def clear_working_dir(repo_dir):
for f in os.listdir(repo_dir):
if f != '.git':
if os.path.isdir(repo_dir / f):
shutil.rmtree(repo_dir / f)
else:
os.remove(repo_dir / f)
@pytest.fixture
def rules_dir():
return Path(__file__).parent.joinpath('resources', 'rules')
JSTS_SONARPEDIA='{"rules-metadata-path": "rules", "languages":["JS", "TS"]}'
MOCK_REPOS=[{'name':'SonarJS',
'versions': [
{'name': '3.3.0.5702',
'date': '2020-03-03 10:00:00',
'files': [['sonarpedia.json', JSTS_SONARPEDIA],
['rules/Sonar_way_profile.json', '{}'],
# MethodName is a lagacy key for S100
['rules/MethodName.json', '{}'], ['rules/S1145.json', '{}'],
# not in the rules directory, so not a rule:
['S200.json', '{}']]},
{'name': '5.0.0.6962',
'date': '2020-05-01 10:00:00',
'files': [['sonarpedia.json', JSTS_SONARPEDIA],
['rules/Sonar_way_profile.json', '{}'],
['rules/S100.json', '{}'], ['rules/S1145.json', '{}'], ['rules/S1192.json', '{}']]},
{'name': '6.7.0.14237',
'date': '2020-06-07 10:00:00',
'files': [['sonarpedia.json', JSTS_SONARPEDIA],
['rules/Sonar_way_profile.json', '{}'],
['rules/S100.json', '{}'], ['rules/S1145.json', '{}'], ['rules/S1192.json', '{}']]},
{'name': '7.0.0.14528',
'date': '2020-07-01 10:00:00',
'files': [['sonarpedia.json', JSTS_SONARPEDIA],
['rules/Sonar_way_profile.json', '{}'],
['rules/S100.json', '{}'], ['rules/S1192.json', '{}'],
# add a CSS analyzer in this version
['css-plugin/sonarpedia.json', '{"rules-metadata-path":"cssr", "languages":["CSS"]}'],
['css-plugin/cssr/S100.json', '{}'],
# add a XML analyzer here too
['xml/sonarpedia.json', '{"rules-metadata-path":"r", "languages":["XML"]}'],
['xml/r/S1000.json', '{}']]},
]},
{'name':'sonar-xml',
'versions': [
{'name': '1.2.0.1342',
'date': '2020-01-02 10:00:00',
'files': [['rules/Sonar_way_profile.json', '{}'],
['sonarpedia.json', '{"rules-metadata-path": "rules", "languages":["XML"]}'],
['rules/S103.json', '{}']]}
]},
{'name':'broken',
'versions': [
{'name': 'v1',
'date': '2020-01-01 01:01:01',
'files': [['sonarpedia.json', 'non-json'], # sonarpedia is non-json
['rules/S100.json', '{}']]}
]}]
def test_mock_tags_are_sorted_chronologically():
for mock_spec in MOCK_REPOS:
prev_date = datetime.strptime('1970-01-01 00:00:01', '%Y-%m-%d %H:%M:%S')
for tag in mock_spec['versions']:
cur_date = datetime.strptime(tag['date'], '%Y-%m-%d %H:%M:%S')
assert prev_date < cur_date
prev_date = cur_date
@pytest.fixture
def mock_git_analyzer_repos(tmpdir):
mocked_repos = {};
for mock_spec in MOCK_REPOS:
repo_name = mock_spec['name']
repo_dir = tmpdir.mkdir('mock-' + repo_name)
repo = Repo.init(str(repo_dir))
with repo.config_writer() as config_writer:
config_writer.set_value('user', 'name', 'originuser')
config_writer.set_value('user', 'email', 'originuser@mock.mock')
for tag in mock_spec['versions']:
clear_working_dir(repo_dir)
for fname, contents in tag['files']:
path = Path(repo_dir / fname)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'x') as f:
f.write(contents)
repo.git.add('--all')
repo.index.commit(f"adding version {tag['name']}", commit_date=tag['date'], author_date=tag['date'])
repo.create_tag(tag['name'])
mocked_repos[repo_name] = repo
def mock_clone_repo(repo_url, repo_name):
return mocked_repos[repo_url.split('/')[-1]]
def precloned_repo(x):
Repo(x)
mock=PropertyMock(side_effect=precloned_repo)
mock.clone_from=PropertyMock(side_effect=mock_clone_repo)
return mock
def test_update_coverage_for_repo_version(tmpdir, rules_dir: Path, mock_git_analyzer_repos):
with pushd(tmpdir), patch('rspec_tools.coverage.Repo', mock_git_analyzer_repos):
VER = '3.3.0.5702'
REPO = 'SonarJS'
update_coverage_for_repo_version(REPO, VER, rules_dir)
coverage = tmpdir.join('covered_rules.json')
assert coverage.exists()
cov = load_json(coverage)
assert 'JAVASCRIPT' in cov
assert 'S100' in cov['JAVASCRIPT']
assert 'MethodName' not in cov['JAVASCRIPT'] # MethodName is a legacy key for S100
assert 'S200' not in cov['JAVASCRIPT'] # S200.json is not in the rules directory in mock
assert cov['JAVASCRIPT']['S100'] == {'since': REPO + ' ' + VER, 'until': REPO + ' ' + VER}
# Running it again changes nothing
update_coverage_for_repo_version(REPO, VER, rules_dir)
assert cov == load_json(coverage)
# Running it for a newer version doesn't change when the rules are first implemented
VER2 = '5.0.0.6962'
update_coverage_for_repo_version(REPO, VER2, rules_dir)
cov_new = load_json(coverage)
assert set(cov['JAVASCRIPT'].keys()).issubset(set(cov_new['JAVASCRIPT'].keys()))
assert cov_new['JAVASCRIPT']['S100']['since'] == REPO + ' ' + VER
assert cov_new['JAVASCRIPT']['S100']['until'] == REPO + ' ' + VER2
assert cov_new['JAVASCRIPT']['S1192']['since'] == REPO + ' ' + VER2
assert cov_new['JAVASCRIPT']['S1192']['until'] == REPO + ' ' + VER2
# For rules supported on master only the 'since' part is kept
update_coverage_for_repo_version(REPO, 'master', rules_dir)
assert load_json(coverage)['JAVASCRIPT']['S100'] == REPO + ' ' + VER
def test_update_coverage_for_repo(tmpdir, rules_dir: Path, mock_git_analyzer_repos):
with pushd(tmpdir), patch('rspec_tools.coverage.Repo', mock_git_analyzer_repos):
REPO = 'SonarJS'
update_coverage_for_repo(REPO, rules_dir)
coverage = tmpdir.join('covered_rules.json')
assert coverage.exists()
cov = load_json(coverage)
assert 'JAVASCRIPT' in cov
assert 'TYPESCRIPT' in cov
assert 'S100' in cov['JAVASCRIPT']
assert 'MethodName' not in cov['JAVASCRIPT'] # MethodName is a legacy key for S100
assert cov['JAVASCRIPT']['S100'] == REPO + ' 3.3.0.5702'
assert 'S1145' in cov['JAVASCRIPT']
assert cov['JAVASCRIPT']['S1145'] == {'since': REPO + ' 3.3.0.5702', 'until': REPO + ' 6.7.0.14237'}
@patch('rspec_tools.coverage.REPOS', ['SonarJS', 'sonar-xml'])
def test_update_coverage_for_all_repos(tmpdir, rules_dir: Path, mock_git_analyzer_repos):
with pushd(tmpdir), patch('rspec_tools.coverage.Repo', mock_git_analyzer_repos):
update_coverage_for_all_repos(rules_dir)
coverage = tmpdir.join('covered_rules.json')
assert coverage.exists()
cov = load_json(coverage)
assert {'JAVASCRIPT', 'TYPESCRIPT', 'XML', 'CSS'} == set(cov.keys())
assert 'S100' in cov['JAVASCRIPT']
assert 'MethodName' not in cov['JAVASCRIPT'] # MethodName is a legacy key for S100
assert {'S100'} == set(cov['CSS'].keys())
assert {'S103', 'S1000'} == set(cov['XML'].keys())
assert cov['XML']['S1000'] == 'SonarJS 7.0.0.14528'
def test_update_coverage_no_sonarpedia(tmpdir, rules_dir: Path, mock_git_analyzer_repos, capsys):
with pushd(tmpdir), patch('rspec_tools.coverage.Repo', mock_git_analyzer_repos):
update_coverage_for_repo_version('broken', 'v1', rules_dir)
assert 'failed to collect implemented rules for' in capsys.readouterr().out
coverage = tmpdir.join('covered_rules.json')
assert coverage.exists()
cov = load_json(coverage)
assert cov == {}
def test_update_coverage_nonexisting_versio(tmpdir, rules_dir: Path, mock_git_analyzer_repos, capsys):
with pushd(tmpdir), patch('rspec_tools.coverage.Repo', mock_git_analyzer_repos):
with pytest.raises(Exception):
update_coverage_for_repo_version('broken', 'non-existing', rules_dir)
assert 'checkout failed' in capsys.readouterr().out