mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Unit Test Improvements (#5087)
* Disable migration testing - Compare how long the unit tests take * Change file - To get unit tests to run * Fix format * Consolidate tasks.py - Remove coverage task - Add --coverage flag to test task * Fix typo * Run migration unit tests if migration files are updated * Fix * Touch migration file - Should cause migration unit tests to be run * Force migration checks for docker build * Prevent default report creation in unit tests - Should save some time * Add simple profiling for plugin loading - Display time taken to load each plugin * Fix to invoke test * Disable get_git_log (for testing) * Disable get_git_path in CI - Might remove this entirely? - For now, bypass for unit testing * Add debug for unit registry - Display time taken to load registry * Don't full-reload unit registry * Adjust migration test workflow - env var updates - change paths-filter output * Fix for migration_test.yaml - Actually need to set the output * env fix * db name * Prevent sleep if in test mode * Reduce sleep time on wait_for_db
This commit is contained in:
parent
693d24b4b6
commit
3b4e20b54a
1
.github/workflows/docker.yaml
vendored
1
.github/workflows/docker.yaml
vendored
@ -78,6 +78,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> docker.dev.env
|
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> docker.dev.env
|
||||||
docker-compose run inventree-dev-server invoke test --disable-pty
|
docker-compose run inventree-dev-server invoke test --disable-pty
|
||||||
|
docker-compose run inventree-dev-server invoke test --migrations --disable-pty
|
||||||
docker-compose down
|
docker-compose down
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
55
.github/workflows/migration_test.yaml
vendored
55
.github/workflows/migration_test.yaml
vendored
@ -16,11 +16,11 @@ on:
|
|||||||
env:
|
env:
|
||||||
python_version: 3.9
|
python_version: 3.9
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
INVENTREE_DB_ENGINE: sqlite3
|
|
||||||
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
|
|
||||||
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
||||||
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
||||||
INVENTREE_BACKUP_DIR: ../test_inventree_backup
|
INVENTREE_BACKUP_DIR: ../test_inventree_backup
|
||||||
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_PLUGINS_ENABLED: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
paths-filter:
|
paths-filter:
|
||||||
@ -28,7 +28,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
server: ${{ steps.filter.outputs.server }}
|
migrations: ${{ steps.filter.outputs.migrations }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
@ -36,17 +36,54 @@ jobs:
|
|||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
server:
|
migrations:
|
||||||
- 'InvenTree/**'
|
- '**/migrations/**'
|
||||||
- 'requirements.txt'
|
- '.github/workflows**'
|
||||||
- 'requirements-dev.txt'
|
|
||||||
- '.github/**'
|
migration-tests:
|
||||||
|
name: Run Migration Unit Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: paths-filter
|
||||||
|
if: needs.paths-filter.outputs.migrations == 'true'
|
||||||
|
|
||||||
|
env:
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||||
|
INVENTREE_DB_NAME: inventree
|
||||||
|
INVENTREE_DB_USER: inventree
|
||||||
|
INVENTREE_DB_PASSWORD: password
|
||||||
|
INVENTREE_DB_HOST: '127.0.0.1'
|
||||||
|
INVENTREE_DB_PORT: 5432
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: inventree
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||||
|
- name: Environment Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
apt-dependency: gettext poppler-utils libpq-dev
|
||||||
|
pip-dependency: psycopg2
|
||||||
|
dev-install: true
|
||||||
|
update: true
|
||||||
|
- name: Run Tests
|
||||||
|
run: invoke test --migrations --report
|
||||||
|
|
||||||
migrations-checks:
|
migrations-checks:
|
||||||
name: Run Database Migrations
|
name: Run Database Migrations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: paths-filter
|
needs: paths-filter
|
||||||
if: needs.paths-filter.outputs.server == 'true'
|
if: needs.paths-filter.outputs.migrations == 'true'
|
||||||
|
|
||||||
|
env:
|
||||||
|
INVENTREE_DB_ENGINE: sqlite3
|
||||||
|
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
2
.github/workflows/qc_checks.yaml
vendored
2
.github/workflows/qc_checks.yaml
vendored
@ -204,7 +204,7 @@ jobs:
|
|||||||
- name: Check Migration Files
|
- name: Check Migration Files
|
||||||
run: python3 ci/check_migration_files.py
|
run: python3 ci/check_migration_files.py
|
||||||
- name: Coverage Tests
|
- name: Coverage Tests
|
||||||
run: invoke coverage
|
run: invoke test --coverage
|
||||||
- name: Upload Coverage Report
|
- name: Upload Coverage Report
|
||||||
uses: coverallsapp/github-action@v2
|
uses: coverallsapp/github-action@v2
|
||||||
with:
|
with:
|
||||||
|
@ -48,7 +48,7 @@ class InvenTreeConfig(AppConfig):
|
|||||||
self.collect_notification_methods()
|
self.collect_notification_methods()
|
||||||
|
|
||||||
# Ensure the unit registry is loaded
|
# Ensure the unit registry is loaded
|
||||||
InvenTree.conversion.reload_unit_registry()
|
InvenTree.conversion.get_unit_registry()
|
||||||
|
|
||||||
if canAppAccessDatabase() or settings.TESTING_ENV:
|
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
self.add_user_on_startup()
|
self.add_user_on_startup()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Helper functions for converting between units."""
|
"""Helper functions for converting between units."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -8,6 +10,9 @@ import pint
|
|||||||
_unit_registry = None
|
_unit_registry = None
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def get_unit_registry():
|
def get_unit_registry():
|
||||||
"""Return a custom instance of the Pint UnitRegistry."""
|
"""Return a custom instance of the Pint UnitRegistry."""
|
||||||
|
|
||||||
@ -26,6 +31,9 @@ def reload_unit_registry():
|
|||||||
This function is called at startup, and whenever the database is updated.
|
This function is called at startup, and whenever the database is updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
t_start = time.time()
|
||||||
|
|
||||||
global _unit_registry
|
global _unit_registry
|
||||||
|
|
||||||
_unit_registry = pint.UnitRegistry()
|
_unit_registry = pint.UnitRegistry()
|
||||||
@ -39,6 +47,9 @@ def reload_unit_registry():
|
|||||||
|
|
||||||
# TODO: Allow for custom units to be defined in the database
|
# TODO: Allow for custom units to be defined in the database
|
||||||
|
|
||||||
|
dt = time.time() - t_start
|
||||||
|
logger.debug(f'Loaded unit registry in {dt:.3f}s')
|
||||||
|
|
||||||
|
|
||||||
def convert_physical_value(value: str, unit: str = None):
|
def convert_physical_value(value: str, unit: str = None):
|
||||||
"""Validate that the provided value is a valid physical quantity.
|
"""Validate that the provided value is a valid physical quantity.
|
||||||
|
@ -18,7 +18,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
while not connected:
|
while not connected:
|
||||||
|
|
||||||
time.sleep(5)
|
time.sleep(2)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connection.ensure_connection()
|
connection.ensure_connection()
|
||||||
|
@ -91,13 +91,15 @@ def check_daily_holdoff(task_name: str, n_days: int = 1) -> bool:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
from InvenTree.ready import isInTestMode
|
||||||
|
|
||||||
if n_days <= 0:
|
if n_days <= 0:
|
||||||
logger.info(f"Specified interval for task '{task_name}' < 1 - task will not run")
|
logger.info(f"Specified interval for task '{task_name}' < 1 - task will not run")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Sleep a random number of seconds to prevent worker conflict
|
# Sleep a random number of seconds to prevent worker conflict
|
||||||
time.sleep(random.randint(1, 5))
|
if not isInTestMode():
|
||||||
|
time.sleep(random.randint(1, 5))
|
||||||
|
|
||||||
attempt_key = f'_{task_name}_ATTEMPT'
|
attempt_key = f'_{task_name}_ATTEMPT'
|
||||||
success_key = f'_{task_name}_SUCCESS'
|
success_key = f'_{task_name}_SUCCESS'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 3.2.18 on 2023-04-17 05:54
|
# Generated by Django 3.2.18 on 2023-04-17 05:55
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -35,7 +35,7 @@ class LabelConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
"""This function is called whenever the label app is loaded."""
|
"""This function is called whenever the label app is loaded."""
|
||||||
if canAppAccessDatabase():
|
if canAppAccessDatabase(allow_test=False):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.create_labels() # pragma: no cover
|
self.create_labels() # pragma: no cover
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Helpers for plugin app."""
|
"""Helpers for plugin app."""
|
||||||
|
|
||||||
import datetime
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
@ -14,8 +13,6 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from dulwich.repo import NotGitRepository, Repo
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
@ -112,25 +109,34 @@ def get_entrypoints():
|
|||||||
def get_git_log(path):
|
def get_git_log(path):
|
||||||
"""Get dict with info of the last commit to file named in path."""
|
"""Get dict with info of the last commit to file named in path."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from dulwich.repo import NotGitRepository, Repo
|
||||||
|
|
||||||
|
from InvenTree.ready import isInTestMode
|
||||||
|
|
||||||
output = None
|
output = None
|
||||||
path = path.replace(str(settings.BASE_DIR.parent), '')[1:]
|
path = path.replace(str(settings.BASE_DIR.parent), '')[1:]
|
||||||
|
|
||||||
try:
|
# only do this if we are not in test mode
|
||||||
walker = Repo.discover(path).get_walker(paths=[path.encode()], max_entries=1)
|
if not isInTestMode(): # pragma: no cover
|
||||||
|
|
||||||
try:
|
try:
|
||||||
commit = next(iter(walker)).commit
|
walker = Repo.discover(path).get_walker(paths=[path.encode()], max_entries=1)
|
||||||
except StopIteration:
|
try:
|
||||||
|
commit = next(iter(walker)).commit
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
output = [
|
||||||
|
commit.sha().hexdigest(),
|
||||||
|
commit.author.decode().split('<')[0][:-1],
|
||||||
|
commit.author.decode().split('<')[1][:-1],
|
||||||
|
datetime.datetime.fromtimestamp(commit.author_time, ).isoformat(),
|
||||||
|
commit.message.decode().split('\n')[0],
|
||||||
|
]
|
||||||
|
except NotGitRepository:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
output = [
|
|
||||||
commit.sha().hexdigest(),
|
|
||||||
commit.author.decode().split('<')[0][:-1],
|
|
||||||
commit.author.decode().split('<')[1][:-1],
|
|
||||||
datetime.datetime.fromtimestamp(commit.author_time, ).isoformat(),
|
|
||||||
commit.message.decode().split('\n')[0],
|
|
||||||
]
|
|
||||||
except NotGitRepository:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
output = 5 * [''] # pragma: no cover
|
output = 5 * [''] # pragma: no cover
|
||||||
|
@ -9,6 +9,7 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, OrderedDict
|
from typing import Dict, List, OrderedDict
|
||||||
|
|
||||||
@ -439,13 +440,16 @@ class PluginsRegistry:
|
|||||||
continue # continue -> the plugin is not loaded
|
continue # continue -> the plugin is not loaded
|
||||||
|
|
||||||
# Initialize package - we can be sure that an admin has activated the plugin
|
# Initialize package - we can be sure that an admin has activated the plugin
|
||||||
logger.info(f'Loading plugin `{plg_name}`')
|
logger.debug(f'Loading plugin `{plg_name}`')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
t_start = time.time()
|
||||||
plg_i: InvenTreePlugin = plg()
|
plg_i: InvenTreePlugin = plg()
|
||||||
logger.debug(f'Loaded plugin `{plg_name}`')
|
dt = time.time() - t_start
|
||||||
|
logger.info(f'Loaded plugin `{plg_name}` in {dt:.3f}s')
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
handle_error(error, log_name='init') # log error and raise it -> disable plugin
|
handle_error(error, log_name='init') # log error and raise it -> disable plugin
|
||||||
|
logger.warning(f"Plugin `{plg_name}` could not be loaded")
|
||||||
|
|
||||||
# Safe extra attributes
|
# Safe extra attributes
|
||||||
plg_i.is_package = getattr(plg_i, 'is_package', False)
|
plg_i.is_package = getattr(plg_i, 'is_package', False)
|
||||||
|
@ -10,7 +10,7 @@ from plugin.models import PluginConfig
|
|||||||
|
|
||||||
|
|
||||||
class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||||
"""Tests the plugin API endpoints."""
|
"""Tests the plugin API endpoints"""
|
||||||
|
|
||||||
roles = [
|
roles = [
|
||||||
'admin.add',
|
'admin.add',
|
||||||
|
@ -25,7 +25,7 @@ class ReportConfig(AppConfig):
|
|||||||
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Create entries for default report templates
|
# Create entries for default report templates
|
||||||
if canAppAccessDatabase(allow_test=True):
|
if canAppAccessDatabase(allow_test=False):
|
||||||
self.create_default_test_reports()
|
self.create_default_test_reports()
|
||||||
self.create_default_build_reports()
|
self.create_default_build_reports()
|
||||||
self.create_default_bill_of_materials_reports()
|
self.create_default_bill_of_materials_reports()
|
||||||
|
52
tasks.py
52
tasks.py
@ -565,9 +565,12 @@ def test_translations(c):
|
|||||||
help={
|
help={
|
||||||
'disable_pty': 'Disable PTY',
|
'disable_pty': 'Disable PTY',
|
||||||
'runtest': 'Specify which tests to run, in format <module>.<file>.<class>.<method>',
|
'runtest': 'Specify which tests to run, in format <module>.<file>.<class>.<method>',
|
||||||
|
'migrations': 'Run migration unit tests',
|
||||||
|
'report': 'Display a report of slow tests',
|
||||||
|
'coverage': 'Run code coverage analysis (requires coverage package)',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test(c, disable_pty=False, runtest=''):
|
def test(c, disable_pty=False, runtest='', migrations=False, report=False, coverage=False):
|
||||||
"""Run unit-tests for InvenTree codebase.
|
"""Run unit-tests for InvenTree codebase.
|
||||||
|
|
||||||
To run only certain test, use the argument --runtest.
|
To run only certain test, use the argument --runtest.
|
||||||
@ -583,8 +586,32 @@ def test(c, disable_pty=False, runtest=''):
|
|||||||
|
|
||||||
pty = not disable_pty
|
pty = not disable_pty
|
||||||
|
|
||||||
# Run coverage tests
|
_apps = ' '.join(apps())
|
||||||
manage(c, f'test --slowreport {runtest}', pty=pty)
|
|
||||||
|
cmd = 'test'
|
||||||
|
|
||||||
|
if runtest:
|
||||||
|
# Specific tests to run
|
||||||
|
cmd += f' {runtest}'
|
||||||
|
else:
|
||||||
|
# Run all tests
|
||||||
|
cmd += f' {_apps}'
|
||||||
|
|
||||||
|
if report:
|
||||||
|
cmd += ' --slowreport'
|
||||||
|
|
||||||
|
if migrations:
|
||||||
|
cmd += ' --tag migration_test'
|
||||||
|
else:
|
||||||
|
cmd += ' --exclude-tag migration_test'
|
||||||
|
|
||||||
|
if coverage:
|
||||||
|
# Run tests within coverage environment, and generate report
|
||||||
|
c.run(f'coverage run {managePyPath()} {cmd}')
|
||||||
|
c.run('coverage html -i')
|
||||||
|
else:
|
||||||
|
# Run simple test runner, without coverage
|
||||||
|
manage(c, cmd, pty=pty)
|
||||||
|
|
||||||
|
|
||||||
@task(help={'dev': 'Set up development environment at the end'})
|
@task(help={'dev': 'Set up development environment at the end'})
|
||||||
@ -629,25 +656,6 @@ def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset")
|
|||||||
setup_dev(c)
|
setup_dev(c)
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def coverage(c):
|
|
||||||
"""Run code-coverage of the InvenTree codebase, using the 'coverage' code-analysis tools.
|
|
||||||
|
|
||||||
Generates a code coverage report (available in the htmlcov directory)
|
|
||||||
"""
|
|
||||||
# Run sanity check on the django install
|
|
||||||
manage(c, 'check')
|
|
||||||
|
|
||||||
# Run coverage tests
|
|
||||||
c.run('coverage run {manage} test {apps}'.format(
|
|
||||||
manage=managePyPath(),
|
|
||||||
apps=' '.join(apps())
|
|
||||||
))
|
|
||||||
|
|
||||||
# Generate coverage report
|
|
||||||
c.run('coverage html -i')
|
|
||||||
|
|
||||||
|
|
||||||
@task(help={
|
@task(help={
|
||||||
'filename': "Output filename (default = 'schema.yml')",
|
'filename': "Output filename (default = 'schema.yml')",
|
||||||
'overwrite': "Overwrite existing files without asking first (default = off/False)",
|
'overwrite': "Overwrite existing files without asking first (default = off/False)",
|
||||||
|
Loading…
Reference in New Issue
Block a user