Skip to content

Commit

Permalink
rust: event file read and write methods in the same module
Browse files Browse the repository at this point in the history
  • Loading branch information
rizsotto committed Dec 15, 2024
1 parent 1bd952d commit eb5343c
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 72 deletions.
5 changes: 2 additions & 3 deletions rust/bear/src/modes/combined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ impl Mode for Combined {
Self::consume_for_analysis(interpreter, semantic_transform, output_writer, envelopes)
})
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.intercept_config, service.address())
.with_context(|| "Failed to create the ipc environment")?;

let status = environment
let status = InterceptEnvironment::new(&self.intercept_config, service.address())
.with_context(|| "Failed to create the ipc environment")?
.execute_build_command(self.command)
.with_context(|| "Failed to execute the build command")?;

Expand Down
30 changes: 16 additions & 14 deletions rust/bear/src/modes/intercept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
use super::Mode;
use crate::ipc::tcp::CollectorOnTcp;
use crate::ipc::{Collector, Envelope};
use crate::output::event::write;
use crate::{args, config};
use anyhow::Context;
use std::fs::OpenOptions;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
Expand Down Expand Up @@ -41,18 +43,19 @@ impl Intercept {

/// Consume events and write them into the output file.
fn write_to_file(
output_file_name: String,
output_file_name: PathBuf,
envelopes: impl IntoIterator<Item = Envelope>,
) -> anyhow::Result<()> {
let mut writer = std::fs::File::create(&output_file_name)
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(output_file_name.as_path())
.map(BufWriter::new)
.with_context(|| format!("Failed to create output file: {:?}", &output_file_name))?;
for envelope in envelopes {
serde_json::to_writer(&mut writer, &envelope).with_context(|| {
format!("Failed to write execution report: {:?}", &output_file_name)
})?;
// TODO: add a newline character to separate the entries
}
.with_context(|| format!("Failed to open file: {:?}", output_file_name))?;

write(file, envelopes)?;

Ok(())
}
}
Expand All @@ -64,11 +67,10 @@ impl Mode for Intercept {
///
/// The exit code is based on the result of the build command.
fn run(self) -> anyhow::Result<ExitCode> {
let output_file_name = self.output.file_name.clone();
let service = CollectorService::new(move |envelopes| {
Self::write_to_file(output_file_name, envelopes)
})
.with_context(|| "Failed to create the ipc service")?;
let output_file = PathBuf::from(self.output.file_name);
let service =
CollectorService::new(move |envelopes| Self::write_to_file(output_file, envelopes))
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.config, service.address())
.with_context(|| "Failed to create the ipc environment")?;

Expand Down
70 changes: 16 additions & 54 deletions rust/bear/src/modes/semantic.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use super::super::semantic;
use crate::ipc::{Envelope, Execution};
use crate::modes::Mode;
use crate::output::event::read;
use crate::output::{OutputWriter, OutputWriterImpl};
use crate::semantic::transformation::Transformation;
use crate::semantic::Transform;
use crate::{args, config};
use anyhow::Context;
use serde_json::de::IoRead;
use serde_json::{Error, StreamDeserializer};
use std::convert::TryFrom;
use std::fs::OpenOptions;
use std::fs::{File, OpenOptions};
use std::io::BufReader;
use std::path::PathBuf;
use std::process::ExitCode;

/// The semantic mode we are deduct the semantic meaning of the
/// executed commands from the build process.
pub struct Semantic {
event_source: EventFileReader,
event_source: BufReader<File>,
interpreter: Box<dyn semantic::Interpreter>,
semantic_transform: Transformation,
output_writer: OutputWriterImpl,
Expand All @@ -32,7 +28,7 @@ impl Semantic {
output: args::BuildSemantic,
config: config::Main,
) -> anyhow::Result<Self> {
let event_source = EventFileReader::try_from(input)?;
let event_source = Self::open(input.file_name.as_str())?;
let interpreter = Self::interpreter(&config)?;
let semantic_transform = Transformation::from(&config.output);
let output_writer = OutputWriterImpl::create(&output, &config.output)?;
Expand All @@ -45,6 +41,16 @@ impl Semantic {
})
}

/// Open the event file for reading.
fn open(file_name: &str) -> anyhow::Result<BufReader<File>> {
let file = OpenOptions::new()
.read(true)
.open(file_name)
.map(BufReader::new)
.with_context(|| format!("Failed to open file: {:?}", file_name))?;
Ok(file)
}

/// Creates an interpreter to recognize the compiler calls.
///
/// Using the configuration we can define which compilers to include and exclude.
Expand Down Expand Up @@ -83,9 +89,8 @@ impl Mode for Semantic {
/// The exit code is based on the result of the output writer.
fn run(self) -> anyhow::Result<ExitCode> {
// Set up the pipeline of compilation database entries.
let entries = self
.event_source
.generate()
let entries = read(self.event_source)
.map(|envelope| envelope.event.execution)
.inspect(|execution| log::debug!("execution: {}", execution))
.flat_map(|execution| self.interpreter.recognize(&execution))
.inspect(|semantic| log::debug!("semantic: {:?}", semantic))
Expand All @@ -98,46 +103,3 @@ impl Mode for Semantic {
}
}
}

/// Responsible for reading the build events from the intercept mode.
///
/// The file syntax is defined by the `events` module, and the parsing logic is implemented there.
/// Here we only handle the file opening and the error handling.
pub struct EventFileReader {
stream: Box<dyn Iterator<Item = Result<Envelope, Error>>>,
}

impl TryFrom<args::BuildEvents> for EventFileReader {
type Error = anyhow::Error;

/// Open the file and create a new instance of the event file reader.
///
/// If the file cannot be opened, the error will be logged and escalated.
fn try_from(value: args::BuildEvents) -> Result<Self, Self::Error> {
let file_name = PathBuf::from(value.file_name);
let file = OpenOptions::new()
.read(true)
.open(file_name.as_path())
.map(BufReader::new)
.with_context(|| format!("Failed to open input file: {:?}", file_name))?;
let stream = Box::new(StreamDeserializer::new(IoRead::new(file)));

Ok(EventFileReader { stream })
}
}

impl EventFileReader {
/// Generate the build events from the file.
///
/// Returns an iterator over the build events. Any error during the reading
/// of the file will be logged and the failed entries will be skipped.
pub fn generate(self) -> impl Iterator<Item = Execution> {
self.stream.filter_map(|result| match result {
Ok(value) => Some(value.event.execution),
Err(error) => {
log::error!("Failed to read event: {:?}", error);
None
}
})
}
}
148 changes: 148 additions & 0 deletions rust/bear/src/output/event/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::ipc::Envelope;
use serde_json::de::IoRead;
use serde_json::StreamDeserializer;
use std::io;

/// Generate the build events from the file.
///
/// Returns an iterator over the build events.
/// Any error will interrupt the reading process and the remaining events will be lost.
pub fn read(reader: impl io::Read) -> impl Iterator<Item = Envelope> {
let stream = StreamDeserializer::new(IoRead::new(reader));
stream.filter_map(|result| match result {
Ok(value) => Some(value),
Err(error) => {
log::error!("Failed to read event: {:?}", error);
None
}
})
}

/// Write the build events to the file.
///
/// Can fail if the events cannot be serialized or written to the file.
/// Any error will interrupt the writing process and the file will be incomplete.
pub fn write(
mut writer: impl io::Write,
envelopes: impl IntoIterator<Item = Envelope>,
) -> Result<(), anyhow::Error> {
for envelope in envelopes {
serde_json::to_writer(&mut writer, &envelope)?;
writer.write_all(b"\n")?;
}
Ok(())
}

#[cfg(test)]
mod test {
use super::*;
use crate::ipc::{Event, Execution, ProcessId, ReporterId};
use crate::vec_of_strings;
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::PathBuf;

#[test]
fn read_write() {
let events = expected_values();

let mut buffer = Vec::new();
write(&mut buffer, events.iter().cloned()).unwrap();
let mut cursor = Cursor::new(buffer);
let read_events: Vec<_> = read(&mut cursor).collect();

assert_eq!(events, read_events);
}

#[test]
fn read_write_empty() {
let events = Vec::<Envelope>::new();

let mut buffer = Vec::new();
write(&mut buffer, events.iter().cloned()).unwrap();
let mut cursor = Cursor::new(buffer);
let read_events: Vec<_> = read(&mut cursor).collect();

assert_eq!(events, read_events);
}

#[test]
fn read_stops_on_errors() {
let line1 = json!({
"rid": 42,
"timestamp": 0,
"event": {
"pid": 11782,
"execution": {
"executable": "/usr/bin/clang",
"arguments": ["clang", "-c", "main.c"],
"working_dir": "/home/user",
"environment": {
"PATH": "/usr/bin",
"HOME": "/home/user"
}
}
}
});
let line2 = json!({"rid": 42 });
let line3 = json!({
"rid": 42,
"timestamp": 273,
"event": {
"pid": 11934,
"execution": {
"executable": "/usr/bin/clang",
"arguments": ["clang", "-c", "output.c"],
"working_dir": "/home/user",
"environment": {}
}
}
});
let content = format!("{}\n{}\n{}\n", line1, line2, line3);

let mut cursor = Cursor::new(content);
let read_events: Vec<_> = read(&mut cursor).collect();

// Only the fist event is read, all other lines are ignored.
assert_eq!(expected_values()[0..1], read_events);
}

const REPORTER_ID: ReporterId = ReporterId(42);

fn expected_values() -> Vec<Envelope> {
vec![
Envelope {
rid: REPORTER_ID,
timestamp: 0,
event: Event {
pid: ProcessId(11782),
execution: Execution {
executable: PathBuf::from("/usr/bin/clang"),
arguments: vec_of_strings!["clang", "-c", "main.c"],
working_dir: PathBuf::from("/home/user"),
environment: HashMap::from([
("PATH".to_string(), "/usr/bin".to_string()),
("HOME".to_string(), "/home/user".to_string()),
]),
},
},
},
Envelope {
rid: REPORTER_ID,
timestamp: 273,
event: Event {
pid: ProcessId(11934),
execution: Execution {
executable: PathBuf::from("/usr/bin/clang"),
arguments: vec_of_strings!["clang", "-c", "output.c"],
working_dir: PathBuf::from("/home/user"),
environment: HashMap::from([]),
},
},
},
]
}
}
3 changes: 2 additions & 1 deletion rust/bear/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use anyhow::{anyhow, Context, Result};
use path_absolutize::Absolutize;

mod clang;
pub mod event;
mod filter;

/// The output writer trait is responsible for writing output file.
Expand Down Expand Up @@ -69,7 +70,7 @@ pub(crate) struct SemanticOutputWriter {
impl OutputWriter for SemanticOutputWriter {
fn run(&self, entries: impl Iterator<Item = semantic::CompilerCall>) -> Result<()> {
let file_name = &self.output;
let file = File::create(&file_name)
let file = File::create(file_name)
.map(BufWriter::new)
.with_context(|| format!("Failed to create file: {:?}", file_name.as_path()))?;

Expand Down

0 comments on commit eb5343c

Please sign in to comment.