Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6dcd1e5
Speed up serialize/deserialize, don't load all tests up front
loganharbour Nov 3, 2025
d824042
Small optimization, don't call expensive method
loganharbour Nov 3, 2025
20c3119
Only set the value if we loaded the test
loganharbour Nov 3, 2025
b8a2704
Limit calls to name and value
loganharbour Nov 3, 2025
d3fe13c
Implement the test iterator separately
loganharbour Nov 3, 2025
a65136b
Fix comments
loganharbour Nov 3, 2025
ee3164b
Use TestName everywhere and add query_test optimization
loganharbour Nov 3, 2025
5f7e69b
Only get the database on demand
loganharbour Nov 3, 2025
07228af
Optimize lookups when all tests have been loaded
loganharbour Nov 3, 2025
bc60ab2
Update results summary with has_test changes
loganharbour Nov 4, 2025
f453b21
Add a timeout to mongo commands
loganharbour Nov 4, 2025
0d44b2f
Add test for not setting check
loganharbour Nov 4, 2025
bee34b4
Add testing for in_place and load_all_tests
loganharbour Nov 4, 2025
739338a
Minor optimization for test iteration
loganharbour Nov 4, 2025
8932f67
Optimization for serialize
loganharbour Nov 4, 2025
b6a3ab6
Optimize the same for serialize
loganharbour Nov 4, 2025
f3fd724
Optimize the test iterator
loganharbour Nov 4, 2025
85ceced
More serialize and deserialize optimization; don't use iterators
loganharbour Nov 4, 2025
a0e8930
Make ruff happy
loganharbour Nov 4, 2025
cd9c533
Deserialize the standard object ID
loganharbour Nov 4, 2025
fe23d74
Pass in a client
loganharbour Nov 4, 2025
4512c08
Optimize json metadata loading
loganharbour Nov 4, 2025
7df0e1d
Revert some of the optimizations
loganharbour Nov 4, 2025
31b35f1
Simplify test
loganharbour Nov 5, 2025
93ed67e
Remove debug print
loganharbour Nov 5, 2025
dd1b90a
More optimization rework
loganharbour Nov 5, 2025
8507ece
Remove unnecessary shabang
loganharbour Nov 5, 2025
80a2e3c
Make black happy, add docstring
loganharbour Nov 5, 2025
d147788
Add tests for not checking data
loganharbour Nov 5, 2025
a8e8168
Move heaver properties into functions
loganharbour Nov 5, 2025
2644e26
Optimize load_all_tests
loganharbour Nov 5, 2025
65b9f55
Remove unused import
loganharbour Nov 5, 2025
eedb3ac
Simplify client and database
loganharbour Nov 6, 2025
7889e3d
Add moosepytest module and move common pytest utilities
loganharbour Nov 3, 2025
06cd0a3
Begin resultsstore rewrite
loganharbour Nov 10, 2025
a51fe66
Begin work on get_all_tests method
loganharbour Nov 10, 2025
f0f5fbe
Move testing to TestHarness
loganharbour Nov 11, 2025
3ad8f98
Continue work with unit tests
loganharbour Nov 11, 2025
c17b074
Use new plugin
loganharbour Nov 11, 2025
703021e
Add tests for auth
loganharbour Nov 11, 2025
b814ba2
Continue with unit test rewrite
loganharbour Nov 12, 2025
d1cef23
Add more testing for resultscollection
loganharbour Nov 12, 2025
503e163
Clean up authentication
loganharbour Nov 12, 2025
ac59182
Remove moved test
loganharbour Nov 12, 2025
1f337bc
Skip init in coverage
loganharbour Nov 12, 2025
5591c4f
Make python 3.11 and older happy
loganharbour Nov 12, 2025
a55f435
Update resultssummary to use new resultsstore
loganharbour Nov 14, 2025
4d595ea
Minor doco improvements
loganharbour Nov 14, 2025
ed2cf6c
Add tests for child getters
loganharbour Nov 14, 2025
dbe5c21
Fix due to changes with resultcollection
loganharbour Nov 14, 2025
aa89948
Expand gold testing
loganharbour Nov 14, 2025
8fd75ad
Remove old gold
loganharbour Nov 14, 2025
9a37cc2
Make all deserialize methods build the object
loganharbour Nov 15, 2025
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
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ include = '''
(
^\/python\/moosecontrol\/.*.py$ |
^\/python\/TestHarness\/resultsstore\/.*.py$ |
^\/python\/TestHarness/tests/test_resultsstore.*.py$
^\/python\/TestHarness\/resultssummary\/.*.py$ |
^\/python\/TestHarness/tests/resultsstore\/.*.py$ |
^\/python\/TestHarness/tests/resultssummary\/.*.py$
)
'''

[tool.ruff]
include = [
"python/moosecontrol/**/*.py",
"python/TestHarness/resultsstore/**/*.py",
"python/TestHarness/tests/test_resultsstore*.py"
"python/TestHarness/resultssummary/**/*.py",
"python/TestHarness/tests/resultsstore/**/*.py",
"python/TestHarness/tests/resultssummary/**/*.py"
]
exclude = ["python/moosecontrol/requests_unixsocket/*.py"]

Expand Down
13 changes: 13 additions & 0 deletions python/TestHarness/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[run]
concurrency = thread
branch = True
source =
resultsstore/*
resultssummary/*
omit =
resultsstore/*/tests
parallel = False

[report]
show_missing = True
fail_under = 100
8 changes: 5 additions & 3 deletions python/TestHarness/TestHarness.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import argparse
import typing
from collections import defaultdict, namedtuple, OrderedDict
from typing import Any, Tuple
from typing import Tuple, Optional

from socket import gethostname

Expand Down Expand Up @@ -224,9 +224,11 @@ class TestHarness:
# 2 - Added 'abs_zero' key to ValidationNumericData
VALIDATION_VERSION = 2

__test__ = False # prevents pytest collection

@staticmethod
def build(argv: list, app_name: str, moose_dir: str, moose_python: str = None,
skip_testroot: bool = False) -> None:
def build(argv: list, app_name: str, moose_dir: str, moose_python: Optional[str] = None,
skip_testroot: bool = False) -> "TestHarness":
# Cannot skip the testroot if we don't have an application name
if skip_testroot and not app_name:
raise ValueError(f'Must provide "app_name" when skip_testroot=True')
Expand Down
13 changes: 13 additions & 0 deletions python/TestHarness/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[pytest]
addopts =
-vvv
--cov=TestHarness.resultsstore
--cov=TestHarness.resultssummary
--cov-config=.coveragerc
--durations=20
testpaths =
tests/resultsstore
tests/resultssummary
markers =
moose: marks tests as requiring moose (use --no-moose to skip)
live_db: marks tests as using a live database (use --no-live-db to skip)
10 changes: 3 additions & 7 deletions python/TestHarness/resultsstore/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ def get_var(key: str) -> Optional[str]:
auth[key] = v
# Have all three set
if len(auth) == 3:
auth["port"] = get_var("port")
port = get_var("port")
auth["port"] = int(port) if port is not None else None
return Authentication(**auth)
# Have one or two but not all three set
if len(auth) != 0:
all_auth_vars = " ".join(map(var_name, all_auth_keys))
raise ValueError(
f'All environment variables "{all_auth_vars}"'
"must be set for authentication"
" must be set for authentication"
)

# Try to get authentication from file
Expand All @@ -85,8 +86,3 @@ def get_var(key: str) -> Optional[str]:
return Authentication(**values)
except Exception as e:
raise Exception(f"Failed to load credentials from '{auth_file}'") from e


def has_authentication(var_prefix: str) -> bool:
"""Check whether or not environment authentication is available."""
return load_authentication(var_prefix) is not None
161 changes: 100 additions & 61 deletions python/TestHarness/resultsstore/civetstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,18 @@
import re
from copy import deepcopy
from datetime import datetime
from typing import Optional, Tuple
from typing import Optional, Sequence, Tuple

from bson import encode
from bson.objectid import ObjectId
from pymongo import MongoClient

from TestHarness.resultsstore.auth import (
Authentication,
has_authentication,
load_authentication,
)
from TestHarness.resultsstore import auth
from TestHarness.resultsstore.utils import (
TestName,
compress_dict,
results_folder_iterator,
results_test_iterator,
mutable_results_folder_iterator,
mutable_results_test_iterator,
)

NoneType = type(None)
Expand Down Expand Up @@ -93,7 +89,7 @@ class CIVETStore:
CIVET_VERSION = 6

@staticmethod
def parse_args() -> argparse.Namespace:
def parse_args(args: Sequence[str]) -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Converts test results from a CIVET run for "
Expand Down Expand Up @@ -125,10 +121,10 @@ def parse_args() -> argparse.Namespace:
default=MAX_RESULT_SIZE,
help="Max size of a result for tests to be stored within it",
)
return parser.parse_args()
return parser.parse_args(args)

@staticmethod
def load_authentication() -> Optional[Authentication]:
def load_authentication() -> Optional[auth.Authentication]:
"""
Load mongo authentication, if available.

Expand All @@ -138,12 +134,7 @@ def load_authentication() -> Optional[Authentication]:
environment from the file set by env var
CIVET_STORE_AUTH_FILE if it is available.
"""
return load_authentication("CIVET_STORE")

@staticmethod
def has_authentication() -> bool:
"""Check whether or not authentication is available."""
return has_authentication("CIVET_STORE")
return auth.load_authentication("CIVET_STORE")

@staticmethod
def get_document_size(doc: dict) -> float:
Expand Down Expand Up @@ -388,7 +379,7 @@ def build(
# Remove skipped tests if requested
num_skipped_tests = 0
if kwargs.get("ignore_skipped"):
for folder in results_folder_iterator(results):
for folder in mutable_results_folder_iterator(results):
for test in folder.test_iterator():
if test.value["status"]["status"] == "SKIP":
num_skipped_tests += 1
Expand All @@ -397,14 +388,13 @@ def build(

# Cleanup each test as needed
num_tests = 0
for test in results_test_iterator(results):
for test in mutable_results_test_iterator(results):
test_values = test.value
num_tests += 1

# Remove all output from results
for key in ["output", "output_files"]:
if key in test_values:
del test_values[key]
del test_values[key]

# Remove keys if requested
for entry in ["status", "timing", "tester"]:
Expand Down Expand Up @@ -439,7 +429,7 @@ def build(
tests = {}
tests_size = 0
oversized_tests = []
for test in results_test_iterator(results):
for test in mutable_results_test_iterator(results):
# Test data in the result entry, separated
test_data = deepcopy(test.value)
# Store and check test size
Expand Down Expand Up @@ -486,39 +476,23 @@ def setup_client() -> MongoClient:
auth.host, auth.port, username=auth.username, password=auth.password
)

def store(
self, database: str, results: dict, base_sha: str, **kwargs
) -> Tuple[ObjectId, Optional[list[ObjectId]]]:
@staticmethod
def _build_database(
result: dict, tests: Optional[dict[TestName, dict]]
) -> Tuple[dict, list[dict]]:
"""
Store the data in the database from a test harness result.
Build entries for the database from the return of build().

Parameters
----------
database : str
The name of the mongo database to store into
results : dict
The results that come from TestHarness JSON result output
base_sha : str
The base commit SHA for the CIVET event

Optional Parameters
-------------------
**kwargs :
See build().

Returns
-------
ObjectID:
The mongo ObjectID of the inserted results document
list[ObjectId] or None:
The mongo ObjectIDs of the inserted test documents, if any;
this will be None if tests are small enough to be stored
within the results document (determined by build())
result : dict
The top level result entry.
tests : Optional[dict[TestName, dict]]
The separate test entires, if any.

"""
assert isinstance(database, str)

result, tests = self.build(results, base_sha, **kwargs)
# Don't modify the input
result = deepcopy(result)

# Assign an ID for the result
result_id = ObjectId()
Expand All @@ -528,8 +502,9 @@ def store(
insert_tests = []
test_ids = None
if tests:
tests = deepcopy(tests)
test_ids = []
for result_test in results_test_iterator(result):
for result_test in mutable_results_test_iterator(result):
# Get the separate test data
test_data = tests[result_test.name]

Expand All @@ -547,23 +522,85 @@ def store(
assert result_test.value is None
result_test.set_value(test_id)

# Do the insertion
return result, insert_tests

def _insert_database(self, database: str, result: dict, tests: list[dict]) -> None:
"""
Insert a result and optionally separate tests into the database.

Used in store(), but kept separate for unit testing.

Parameters
----------
database : str
The name of the mongo database to store into.
result : dict
The results to store.
tests : list[dict]
The separate tests to store.

"""
assert isinstance(database, str)
assert isinstance(result, dict)
assert isinstance(tests, list)
result_id = result["_id"]
assert isinstance(result_id, ObjectId)

with self.setup_client() as client:
db = client[database]

inserted_result = db.results.insert_one(result)
assert inserted_result.acknowledged
assert result_id == inserted_result.inserted_id
print(f"Inserted result {result_id} into {database}")
assert inserted_result.inserted_id == result_id
print(f"Inserted result {inserted_result.inserted_id} into {database}")

if insert_tests:
inserted_tests = db.tests.insert_many(insert_tests)
if tests:
inserted_tests = db.tests.insert_many(tests)
assert inserted_tests.acknowledged
assert isinstance(test_ids, list)
assert set(test_ids) == set(inserted_tests.inserted_ids)
print(f"Inserted {len(test_ids)} tests into {database}")
print(f"Inserted {len(tests)} tests into {database}")

def store(
self, database: str, results: dict, base_sha: str, **kwargs
) -> Tuple[ObjectId, Optional[list[ObjectId]]]:
"""
Store the data in the database from a test harness result.

Parameters
----------
database : str
The name of the mongo database to store into
results : dict
The results that come from TestHarness JSON result output
base_sha : str
The base commit SHA for the CIVET event

Optional Parameters
-------------------
**kwargs :
See build().

Returns
-------
ObjectID:
The mongo ObjectID of the inserted results document
list[ObjectId] or None:
The mongo ObjectIDs of the inserted test documents, if any;
this will be None if tests are small enough to be stored
within the results document (determined by build())

"""
# Get the data
result, tests = self.build(results, base_sha, **kwargs)

return result_id, test_ids
# Build for storage in the database
insert_result, insert_tests = self._build_database(result, tests)

# Do the insertion
self._insert_database(database, insert_result, insert_tests)

return insert_result["_id"], (
[v["_id"] for v in insert_tests] if insert_tests else None
)

def main(
self, result_path: str, database: str, base_sha: str, **kwargs
Expand Down Expand Up @@ -610,6 +647,8 @@ def main(
return self.store(database, results, base_sha, **kwargs)


if __name__ == "__main__":
args = CIVETStore.parse_args()
if __name__ == "__main__": # pragma: no cover
from sys import argv

args = CIVETStore.parse_args(argv[1:])
CIVETStore().main(**vars(args))
Loading