-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add models and task for failed test ingestion #197
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #197 +/- ##
==========================================
- Coverage 98.13% 98.12% -0.01%
==========================================
Files 370 373 +3
Lines 29996 30480 +484
==========================================
+ Hits 29437 29909 +472
- Misses 559 571 +12
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Codecov ReportAttention:
Changes have been made to critical files, which contain lines commonly executed in production. Learn more Additional details and impacted files@@ Coverage Diff @@
## main #197 +/- ##
==========================================
+ Coverage 98.11% 98.12% +0.01%
==========================================
Files 401 373 -28
Lines 30697 30480 -217
==========================================
- Hits 30117 29909 -208
+ Misses 580 571 -9
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 74 files with indirect coverage changes This change has been scanned for critical changes. Learn more |
a351e98
to
99fe006
Compare
82cc9a5
to
76ea63d
Compare
a68c001
to
fb700f2
Compare
Codecov ReportAttention: @@ Coverage Diff @@
## main #197 +/- ##
==========================================
- Coverage 98.13% 98.12% -0.01%
==========================================
Files 370 373 +3
Lines 29996 30480 +484
==========================================
+ Hits 29437 29909 +472
- Misses 559 571 +12
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Codecov Report
@@ Coverage Diff @@
## main #197 +/- ##
==========================================
- Coverage 98.13% 98.12% -0.01%
==========================================
Files 370 373 +3
Lines 29996 30480 +484
==========================================
+ Hits 29437 29909 +472
- Misses 559 571 +12
Flags with carried forward coverage won't be shown. Click here to find out more.
|
037f374
to
eb250a6
Compare
tasks/test_results_processor.py
Outdated
): | ||
for testrun in testrun_list: | ||
test = ( | ||
db_session.query(Test) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an n+1 query. Query for the list of tests outside the loop. Then check for existence in the loop.
tasks/test_results_processor.py
Outdated
elif b"pytest" in first_line: | ||
testrun_list = parse_pytest_reportlog(file_content) | ||
else: | ||
testrun_list = parse_vitest_json(file_content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we maybe match something for vitest and then "else" raise a not supported error?
This might get weird otherwise.
769d55a
to
b493ab3
Compare
tasks/test_results_processor.py
Outdated
parsed_testruns: List[Testrun] = self.process_individual_arg( | ||
upload_obj, upload_obj.report.commit.repository | ||
) | ||
except Exception: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine to leave a catch all exception for starters so we avoid crashing the task while we battle test it. But please be explicit about it, as having a catch-all exception is not good practice.
Also the warning message won't be helpful when debugging. Make sure to include the stack trace so we know what to fix (in case there's anything)
tasks/test_results_processor.py
Outdated
return {"successful": False} | ||
|
||
# concat existing and new test information | ||
testrun_list += parsed_testruns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the upload
task it seems that you decided to go with a group
, that is parallel in nature (meaning we might be processing multiple uploads at the same time for the same commit).
This indicates that it's ok to be saving data concurrently for the same commit. I wonder then if it's necessary to accumulate the parsing results in testrun_list
prior to saving them. I think this list might become possibly big (we know of users that have +100K tests, for example), and this seems like it could use a fairly large amount of data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could save the instances as soon as we parse them, but for tests I think it's better to avoid doing it on every iteration since we will be getting the list of all tests for that repository
tasks/test_results_processor.py
Outdated
test_set = set() | ||
for test in repo_tests: | ||
test_set.add(f"{test.testsuite}::{test.name}") | ||
for testrun in testrun_list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can leverage https://docs.sqlalchemy.org/en/13/orm/session_api.html#sqlalchemy.orm.session.Session.merge session.merge
here to offload this logic to the database...
I have a feeling it'd be more efficient, if possible.
EDIT probably we can't cause it looks at the primary key of the instance and ... well we don't have an instance yet 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we could upsert https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#insert-on-conflict-upsert
tasks/test_results_processor.py
Outdated
# TODO: improve report matching capabilities | ||
# use file extensions? | ||
# maybe do the matching in the testing result parser lib? | ||
first_line = bytes("".join(first_line.decode("utf-8").split()), "utf-8") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] report matching should be in its own function
tasks/test_results_processor.py
Outdated
elif first_line.startswith(b'"{"numTotalTestSuites":'): | ||
testrun_list = parse_vitest_json(file_content) | ||
except Exception as e: | ||
log.warning(f"Error parsing: {file_content.decode()}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use a static message an add extra args to the logger. That will make it easier to search for logs and it's the norm for worker logs
{ | ||
"duration_seconds": 0.001, | ||
"name": "api.temp.calculator.test_calculator::test_add", | ||
"outcome": "Outcome.Pass", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this "Outcome.Pass" defined? In the model the field is an Integer, but I can't find the Enum for it....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's defined in the test-results-parser
library
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we import the type definition into the python code? nice... works like an enum or does it think it's just a string?
EDIT ah I see the docs https://pyo3.rs/main/class
very nice very nice
f296188
to
37f85b1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first off: awesome work on the feature, i think the code is designed pretty well and i think we'll be able to build on it well for flaky test detection, etc
just a callout: we don't have a pattern in place or all the missing pieces we need to avoid loading everything into memory here, so beware that this implementation is at risk of the same performance problems coverage is until we address that
archive_service.read_file()
reads into memory, we'd need to save to diskjson.loads()
doesn't stream, we'd need another json parser- (above is enough to avoid loading more than one results file into memory at once)
- (if we are worried individual results files will be enormous we also need the following)
zlib
appears to have a streaming decompression util https://docs.python.org/3/library/zlib.html#zlib.decompressobj- a streaming base64 decoder
- a rework of the rust code to take a bytes stream abstraction instead of a concrete
Vec<u8>
or&[u8]
tasks/test_results_processor.py
Outdated
log.error( | ||
"File did not match any parser format", | ||
extra=dict( | ||
file_content=file_bytes.read().decode(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will blow up, maybe only print the first 300 chars or something
tasks/test_results_processor.py
Outdated
log.error( | ||
"Error parsing test result file", | ||
extra=dict( | ||
file_content=file_content.decode(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will blow up, maybe limit to 300 chars or something
tasks/test_results_processor.py
Outdated
test_set = set() | ||
for test in repo_tests: | ||
test_set.add(f"{test.testsuite}::{test.name}") | ||
for testrun in testrun_list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we could upsert https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#insert-on-conflict-upsert
d522306
to
28fd629
Compare
testsuite = Column(types.String(256), nullable=False) | ||
|
||
__table_args__ = ( | ||
UniqueConstraint( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since API runs migrations we'll need a change there to actually create this constraint if that hasn't been done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if id_
contains all of these things and it's a proper PK then this constraint may not be necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
codecov/codecov-api#357 because id_
is just a sha256 hash of all of those
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
un-accepting until we talk about whether this perf change is feasible quickly, worth blocking for, or should be deferred
database/models/reports.py
Outdated
|
||
class TestInstance(CodecovBaseModel, MixinBaseClass): | ||
__tablename__ = "reports_testinstance" | ||
test_id = Column(types.Integer, ForeignKey("reports_test.id")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hold up, if we change this to a hash of the test suite + test name we might be able to unlock performance improvements
the reason we can't do the db work in the processor task is because we need to synchronize to get/create records of the Test
model because we need to put its id on the TestInstance
model. if the ID could be computed from the test suite + test name, then we don't need to synchronize with other tasks to create our TestInstance
records
as for creating the corresponding Test
records, we could do INSERT ... ON CONFLICT DO NOTHING
or INSERT ... WHERE NOT EXISTS (...)
to attempt inserts in a fire-and-forget way and the database will handle synchronizing internally
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would have to hash the env as well but I agree that this may get rid of the issue where we want to use a returning
clause
if processor_task_group: | ||
checkpoint_data = None | ||
if checkpoints: | ||
checkpoints.log(UploadFlow.INITIAL_PROCESSING_COMPLETE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we need to use different flows for coverage, bundles, and test results. we have totally different expectations/norms for how long each takes, and putting them together in one pool makes the metrics useless
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
…_list Going to move the save report part to the finisher to avoid concurrency issues
This commit moves the writes in the test result finisher back to the test result processor task. The first step is to change the primary key of the Test object to a text field. This is so we can use a computed primary key for Test objects. By computing the test ID using the generate_test_id function, we avoid having to use a returning clause from the insert on conflict do nothing for Test objects. The timestamp field on the TestInstance object has also been removed as its replaced by using the upload created_at to determine the order of TestInstance objects to be showed in the PR comment. Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
0deb7c9
to
e37e0e6
Compare
we don't want to fail the entire upload parsing because one test result file failed to be parsed. Signed-off-by: joseph-sentry <joseph.sawaya@sentry.io>
Depends on: codecov/test-results-parser#4