Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mod_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""REST API module for test-result consumption."""
135 changes: 135 additions & 0 deletions mod_api/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Versioned REST API endpoints for test-result data."""

from typing import Any, Dict, List

from flask import Blueprint, jsonify
from sqlalchemy import and_

from exceptions import TestNotFoundException
from mod_regression.models import Category, RegressionTestOutput, regressionTestLinkTable
from mod_test.controllers import get_test_results
from mod_test.models import Test, TestResultFile

mod_api = Blueprint('api', __name__)


def _require_test(test_id: int) -> Test:
test = Test.query.filter(Test.id == test_id).first()
if test is None:
raise TestNotFoundException(f"Test with id {test_id} does not exist")
return test


@mod_api.errorhandler(TestNotFoundException)
def test_not_found(error: TestNotFoundException):
return jsonify({'status': 'failure', 'error': error.message}), 404


@mod_api.route('/v1/tests/<int:test_id>/summary', methods=['GET'])
def test_summary(test_id: int):
test = _require_test(test_id)
customized_ids = test.get_customized_regressiontests()
return jsonify({
'status': 'success',
'data': {
'test_id': test.id,
'platform': test.platform.value,
'test_type': test.test_type.value,
'commit': test.commit,
'pr_nr': test.pr_nr,
'finished': test.finished,
'failed': test.failed,
'sample_progress': {
'current': len(test.results),
'total': len(customized_ids),
'percentage': int((len(test.results) / len(customized_ids)) * 100) if customized_ids else 0,
},
},
})


@mod_api.route('/v1/tests/<int:test_id>/results', methods=['GET'])
def test_results(test_id: int):
test = _require_test(test_id)
categories: List[Dict[str, Any]] = []
for entry in get_test_results(test):
category = entry['category']
tests: List[Dict[str, Any]] = []
for category_test in entry['tests']:
result = category_test['result']
tests.append({
'regression_test_id': category_test['test'].id,
'command': category_test['test'].command,
'expected_rc': category_test['test'].expected_rc if result is None else result.expected_rc,
'exit_code': None if result is None else result.exit_code,
'runtime': None if result is None else result.runtime,
'error': category_test['error'],
})
categories.append({
'id': category.id,
'name': category.name,
'error': entry['error'],
'tests': tests,
})

return jsonify({'status': 'success', 'data': categories})


@mod_api.route('/v1/tests/<int:test_id>/files', methods=['GET'])
def test_result_files(test_id: int):
test = _require_test(test_id)
files = TestResultFile.query.filter(TestResultFile.test_id == test.id).all()
data = [{
'regression_test_id': item.regression_test_id,
'regression_test_output_id': item.regression_test_output_id,
'expected_hash': item.expected,
'got_hash': item.got,
} for item in files]
return jsonify({'status': 'success', 'data': data})


@mod_api.route('/v1/tests/<int:test_id>/progress', methods=['GET'])
def test_progress(test_id: int):
test = _require_test(test_id)
progress = [{
'status': entry.status.value,
'message': entry.message,
'timestamp': entry.timestamp.isoformat(),
} for entry in test.progress]
current_step = progress[-1]['status'] if progress else 'unknown'
return jsonify({
'status': 'success',
'data': {
'summary': {
'complete': test.finished,
'failed': test.failed,
'current_step': current_step,
'event_count': len(progress),
},
'events': progress,
},
})


@mod_api.route('/v1/categories', methods=['GET'])
def categories():
populated_categories = regressionTestLinkTable.select().with_only_columns(
regressionTestLinkTable.c.category_id
).subquery()
category_rows = Category.query.filter(Category.id.in_(populated_categories)).order_by(Category.name.asc()).all()

data = []
for category in category_rows:
active_outputs = RegressionTestOutput.query.filter(and_(
RegressionTestOutput.regression_id.in_([rt.id for rt in category.regression_tests]),
RegressionTestOutput.ignore.is_(False),
)).count()
data.append({
'id': category.id,
'name': category.name,
'description': category.description,
'regression_test_count': len(category.regression_tests),
'output_file_count': active_outputs,
})

return jsonify({'status': 'success', 'data': data})
2 changes: 2 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from mod_customized.controllers import mod_customized
from mod_health.controllers import mod_health
from mod_home.controllers import mod_home
from mod_api.controllers import mod_api
from mod_regression.controllers import mod_regression
from mod_sample.controllers import mod_sample
from mod_test.controllers import mod_test
Expand Down Expand Up @@ -273,3 +274,4 @@ def teardown(exception: Optional[Exception]):
app.register_blueprint(mod_ci)
app.register_blueprint(mod_customized, url_prefix='/custom')
app.register_blueprint(mod_health)
app.register_blueprint(mod_api, url_prefix='/api')
1 change: 1 addition & 0 deletions tests/test_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""API endpoint tests."""
73 changes: 73 additions & 0 deletions tests/test_api/test_controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Tests for REST API endpoints."""

from tests.base import BaseTestCase


class TestApiControllers(BaseTestCase):
"""API test coverage for v1 endpoints."""

def test_summary_success(self):
response = self.app.test_client().get('/api/v1/tests/1/summary')
self.assertEqual(response.status_code, 200)
payload = response.json
self.assertEqual(payload['status'], 'success')
self.assertEqual(payload['data']['test_id'], 1)
self.assertEqual(payload['data']['sample_progress']['current'], 2)

def test_summary_not_found(self):
response = self.app.test_client().get('/api/v1/tests/9999/summary')
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json['status'], 'failure')

def test_results_success(self):
response = self.app.test_client().get('/api/v1/tests/1/results')
self.assertEqual(response.status_code, 200)
payload = response.json
self.assertEqual(payload['status'], 'success')
self.assertGreaterEqual(len(payload['data']), 1)
first_category = payload['data'][0]
self.assertIn('tests', first_category)
first_test = first_category['tests'][0]
self.assertIn('expected_rc', first_test)
self.assertIn('exit_code', first_test)
if first_test['exit_code'] is not None:
self.assertEqual(first_test['expected_rc'], first_test['exit_code'])

def test_results_not_found(self):
response = self.app.test_client().get('/api/v1/tests/9999/results')
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json['status'], 'failure')

def test_files_success(self):
response = self.app.test_client().get('/api/v1/tests/1/files')
self.assertEqual(response.status_code, 200)
payload = response.json
self.assertEqual(payload['status'], 'success')
self.assertEqual(len(payload['data']), 2)
self.assertIn('expected_hash', payload['data'][0])
self.assertIn('got_hash', payload['data'][0])

def test_files_not_found(self):
response = self.app.test_client().get('/api/v1/tests/9999/files')
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json['status'], 'failure')

def test_progress_success(self):
response = self.app.test_client().get('/api/v1/tests/1/progress')
self.assertEqual(response.status_code, 200)
payload = response.json
self.assertEqual(payload['status'], 'success')
self.assertEqual(len(payload['data']['events']), 3)
self.assertIn('current_step', payload['data']['summary'])

def test_progress_not_found(self):
response = self.app.test_client().get('/api/v1/tests/9999/progress')
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json['status'], 'failure')

def test_categories_success(self):
response = self.app.test_client().get('/api/v1/categories')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['status'], 'success')
names = {item['name'] for item in response.json['data']}
self.assertIn('Broken', names)