Skip to content

Commit 12ee764

Browse files
committed
get closer to the existing parser interface dealing with report builders
1 parent e0dd890 commit 12ee764

File tree

4 files changed

+140
-25
lines changed

4 files changed

+140
-25
lines changed

core/benches/pyreport.rs

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, hint::black_box};
1+
use std::collections::HashMap;
22

33
use codecov_rs::{
44
parsers::pyreport::{chunks, chunks_serde, report_json},
@@ -109,8 +109,13 @@ fn simple_chunks_serde() {
109109
b"{}\n<<<<< end_of_header >>>>>\n{}\n[1, null, [[0, 1]]]\n\n<<<<< end_of_chunk >>>>>\n{}\n[1, null, [[0, 1]]]\n[1, null, [[0, 1]]]\n",
110110
];
111111

112+
let report_json = report_json::ParsedReportJson {
113+
files: Default::default(),
114+
sessions: Default::default(),
115+
};
116+
112117
for input in chunks {
113-
parse_chunks_file_serde(input)
118+
parse_chunks_file_serde(input, &report_json);
114119
}
115120
}
116121

@@ -121,17 +126,18 @@ fn complex_chunks_serde(bencher: Bencher) {
121126
let chunks =
122127
load_fixture("pyreport/large/worker-c71ddfd4cb1753c7a540e5248c2beaa079fc3341-chunks.txt");
123128

124-
bencher.bench(|| parse_chunks_file_serde(&chunks));
129+
// parsing the chunks depends on having loaded the `report_json`
130+
let report = load_fixture(
131+
"pyreport/large/worker-c71ddfd4cb1753c7a540e5248c2beaa079fc3341-report_json.json",
132+
);
133+
let report_json = parse_report_json(&report);
134+
135+
bencher.bench(|| parse_chunks_file_serde(&chunks, &report_json));
125136
}
126137

127-
fn parse_chunks_file_serde(input: &[u8]) {
128-
let chunks_file = chunks_serde::ChunksFile::new(input).unwrap();
129-
let mut chunks = chunks_file.chunks();
130-
while let Some(mut chunk) = chunks.next_chunk().unwrap() {
131-
while let Some(line) = chunk.next_line().unwrap() {
132-
black_box(line);
133-
}
134-
}
138+
fn parse_chunks_file_serde(input: &[u8], report_json: &report_json::ParsedReportJson) {
139+
let mut report_builder = TestReportBuilder::default();
140+
chunks_serde::parse_chunks_file(input, report_json, &mut report_builder).unwrap();
135141
}
136142

137143
#[track_caller]

core/src/error.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use thiserror::Error;
22

3+
use crate::parsers::pyreport::chunks_serde::ChunksFileParseError;
4+
35
pub type Result<T, E = CodecovError> = std::result::Result<T, E>;
46

57
#[derive(Error, Debug)]
@@ -26,4 +28,7 @@ pub enum CodecovError {
2628
#[cfg(feature = "pyreport")]
2729
#[error("failed to convert sqlite to pyreport: '{0}'")]
2830
PyreportConversionError(String),
31+
32+
#[error(transparent)]
33+
ChunksFileParseError(#[from] ChunksFileParseError),
2934
}

core/src/parsers/pyreport/chunks_serde.rs

+101-14
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,84 @@ use std::{collections::HashMap, fmt, mem, sync::OnceLock};
3737
use memchr::{memchr, memmem};
3838
use serde::{de, de::IgnoredAny, Deserialize};
3939

40-
use crate::report::pyreport::{CHUNKS_FILE_END_OF_CHUNK, CHUNKS_FILE_HEADER_TERMINATOR};
40+
use super::report_json::ParsedReportJson;
41+
use crate::{
42+
error::CodecovError,
43+
report::{
44+
models,
45+
pyreport::{
46+
types::{self, PyreportCoverage, ReportLine},
47+
CHUNKS_FILE_END_OF_CHUNK, CHUNKS_FILE_HEADER_TERMINATOR,
48+
},
49+
Report, ReportBuilder,
50+
},
51+
};
52+
53+
pub fn parse_chunks_file<B, R>(
54+
input: &[u8],
55+
_report_json: &ParsedReportJson,
56+
builder: &mut B,
57+
) -> Result<(), CodecovError>
58+
where
59+
B: ReportBuilder<R>,
60+
R: Report,
61+
{
62+
let chunks_file = ChunksFile::new(input)?;
63+
64+
let mut labels_index = HashMap::with_capacity(chunks_file.labels_index().len());
65+
for (index, name) in chunks_file.labels_index() {
66+
let context = builder.insert_context(name)?;
67+
labels_index.insert(index.clone(), context.id);
68+
}
69+
70+
let mut report_lines = vec![];
71+
72+
let mut chunks = chunks_file.chunks();
73+
while let Some(mut chunk) = chunks.next_chunk()? {
74+
let mut line_no = 0;
75+
report_lines.clear();
76+
while let Some(line) = chunk.next_line()? {
77+
line_no += 1;
78+
if let Some(line) = line {
79+
let coverage_type = match line.1.unwrap_or_default() {
80+
CoverageType::Line => models::CoverageType::Line,
81+
CoverageType::Branch => models::CoverageType::Branch,
82+
CoverageType::Method => models::CoverageType::Method,
83+
};
84+
let sessions = line
85+
.2
86+
.into_iter()
87+
.map(|session| types::LineSession {
88+
session_id: session.0,
89+
coverage: session.1.into(),
90+
branches: None, // TODO
91+
partials: None, // TODO
92+
complexity: None, // TODO
93+
})
94+
.collect();
95+
96+
let mut report_line = ReportLine {
97+
line_no,
98+
coverage: line.0.into(),
99+
coverage_type,
100+
sessions,
101+
_messages: None,
102+
_complexity: None,
103+
datapoints: None, // TODO
104+
};
105+
report_line.normalize();
106+
report_lines.push(report_line);
107+
}
108+
}
109+
// TODO:
110+
// utils::save_report_lines()?;
111+
}
112+
113+
Ok(())
114+
}
41115

42116
#[derive(Debug, thiserror::Error)]
43-
pub enum ParserError {
117+
pub enum ChunksFileParseError {
44118
#[error("unexpected EOF")]
45119
UnexpectedEof,
46120
#[error("unexpected input")]
@@ -53,12 +127,12 @@ pub enum ParserError {
53127
InvalidLineRecord(#[source] serde_json::Error),
54128
}
55129

56-
impl PartialEq for ParserError {
130+
impl PartialEq for ChunksFileParseError {
57131
fn eq(&self, other: &Self) -> bool {
58132
core::mem::discriminant(self) == core::mem::discriminant(other)
59133
}
60134
}
61-
impl Eq for ParserError {}
135+
impl Eq for ChunksFileParseError {}
62136

63137
#[derive(Debug)]
64138
pub struct ChunksFile<'d> {
@@ -67,16 +141,16 @@ pub struct ChunksFile<'d> {
67141
}
68142

69143
impl<'d> ChunksFile<'d> {
70-
pub fn new(mut input: &'d [u8]) -> Result<Self, ParserError> {
144+
pub fn new(mut input: &'d [u8]) -> Result<Self, ChunksFileParseError> {
71145
static HEADER_FINDER: OnceLock<memmem::Finder> = OnceLock::new();
72146
let header_finder =
73147
HEADER_FINDER.get_or_init(|| memmem::Finder::new(CHUNKS_FILE_HEADER_TERMINATOR));
74148

75149
let file_header = if let Some(pos) = header_finder.find(input) {
76150
let header_bytes = &input[..pos];
77151
input = &input[pos + header_finder.needle().len()..];
78-
let file_header: FileHeader =
79-
serde_json::from_slice(header_bytes).map_err(ParserError::InvalidFileHeader)?;
152+
let file_header: FileHeader = serde_json::from_slice(header_bytes)
153+
.map_err(ChunksFileParseError::InvalidFileHeader)?;
80154
file_header
81155
} else {
82156
FileHeader::default()
@@ -99,7 +173,7 @@ pub struct Chunks<'d> {
99173
}
100174

101175
impl<'d> Chunks<'d> {
102-
pub fn next_chunk(&mut self) -> Result<Option<Chunk<'d>>, ParserError> {
176+
pub fn next_chunk(&mut self) -> Result<Option<Chunk<'d>>, ChunksFileParseError> {
103177
if self.input.is_empty() {
104178
return Ok(None);
105179
}
@@ -123,9 +197,10 @@ impl<'d> Chunks<'d> {
123197
}));
124198
}
125199

126-
let header_bytes = next_line(&mut chunk_bytes).ok_or(ParserError::UnexpectedInput)?;
127-
let chunk_header: ChunkHeader =
128-
serde_json::from_slice(header_bytes).map_err(ParserError::InvalidFileHeader)?;
200+
let header_bytes =
201+
next_line(&mut chunk_bytes).ok_or(ChunksFileParseError::UnexpectedInput)?;
202+
let chunk_header: ChunkHeader = serde_json::from_slice(header_bytes)
203+
.map_err(ChunksFileParseError::InvalidFileHeader)?;
129204

130205
Ok(Some(Chunk {
131206
chunk_header,
@@ -144,7 +219,7 @@ impl<'d> Chunk<'d> {
144219
&self.chunk_header.present_sessions
145220
}
146221

147-
pub fn next_line(&mut self) -> Result<Option<Option<LineRecord>>, ParserError> {
222+
pub fn next_line(&mut self) -> Result<Option<Option<LineRecord>>, ChunksFileParseError> {
148223
let Some(line) = next_line(&mut self.input) else {
149224
return Ok(None);
150225
};
@@ -154,7 +229,7 @@ impl<'d> Chunk<'d> {
154229
}
155230

156231
let line_record: LineRecord =
157-
serde_json::from_slice(line).map_err(ParserError::InvalidLineRecord)?;
232+
serde_json::from_slice(line).map_err(ChunksFileParseError::InvalidLineRecord)?;
158233
return Ok(Some(Some(line_record)));
159234
}
160235
}
@@ -217,7 +292,7 @@ pub struct LineRecord(
217292
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
218293
pub struct LineSession(
219294
/// session id
220-
u32,
295+
usize,
221296
/// coverage
222297
Coverage,
223298
/// TODO: branches
@@ -260,6 +335,18 @@ pub enum Coverage {
260335
HitCount(u32),
261336
}
262337

338+
impl Into<PyreportCoverage> for Coverage {
339+
fn into(self) -> PyreportCoverage {
340+
match self {
341+
Coverage::Partial => PyreportCoverage::Partial(),
342+
Coverage::BranchTaken(covered, total) => {
343+
PyreportCoverage::BranchesTaken { covered, total }
344+
}
345+
Coverage::HitCount(hits) => PyreportCoverage::HitCount(hits),
346+
}
347+
}
348+
}
349+
263350
impl<'de> Deserialize<'de> for Coverage {
264351
fn deserialize<D>(deserializer: D) -> Result<Coverage, D::Error>
265352
where

core/src/report/pyreport/types.rs

+17
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ pub struct ReportLine {
184184
pub datapoints: Option<Option<HashMap<u32, CoverageDatapoint>>>,
185185
}
186186

187+
impl ReportLine {
188+
pub fn normalize(&mut self) {
189+
// Fix issues like recording branch coverage with `CoverageType::Method`
190+
let (correct_coverage, correct_type) =
191+
normalize_coverage_measurement(&self.coverage, &self.coverage_type);
192+
self.coverage = correct_coverage;
193+
self.coverage_type = correct_type;
194+
195+
// Fix the `coverage` values in each `LineSession` as well
196+
for line_session in &mut self.sessions {
197+
let (correct_coverage, _) =
198+
normalize_coverage_measurement(&line_session.coverage, &self.coverage_type);
199+
line_session.coverage = correct_coverage;
200+
}
201+
}
202+
}
203+
187204
/// Account for some quirks and malformed data. See code comments for details.
188205
pub(crate) fn normalize_coverage_measurement(
189206
coverage: &PyreportCoverage,

0 commit comments

Comments
 (0)