feat(solc): add scoped reporter (#1000)
* feat(solc): add scoped reporter * fix: race in tests
This commit is contained in:
parent
0c42c23746
commit
1ac0b49ac3
|
@ -1,17 +1,52 @@
|
||||||
//! Subscribe to events in the compiler pipeline
|
//! Subscribe to events in the compiler pipeline
|
||||||
|
//!
|
||||||
|
//! The _reporter_ is the component of the [`Project::compile()`] pipeline which is responsible
|
||||||
|
//! for reporting on specific steps in the process.
|
||||||
|
//!
|
||||||
|
//! By default, the current reporter is a noop that does
|
||||||
|
//! nothing.
|
||||||
|
//!
|
||||||
|
//! To use another report implementation, it must be set as the current reporter.
|
||||||
|
//! There are two methods for doing so: [`with_scoped`] and
|
||||||
|
//! [`set_global`]. `with_scoped` sets the reporter for the
|
||||||
|
//! duration of a scope, while `set_global` sets a global default report
|
||||||
|
//! for the entire process.
|
||||||
|
|
||||||
|
// https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs
|
||||||
|
|
||||||
use crate::{CompilerInput, CompilerOutput, Solc};
|
use crate::{CompilerInput, CompilerOutput, Solc};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use std::{
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
cell::RefCell,
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt,
|
fmt,
|
||||||
path::Path,
|
path::Path,
|
||||||
|
ptr::NonNull,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CURRENT_STATE: State = State {
|
||||||
|
scoped: RefCell::new(Report::none()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static EXISTS: AtomicBool = AtomicBool::new(false);
|
||||||
|
static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
// tracks the state of `GLOBAL_REPORTER`
|
||||||
|
static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);
|
||||||
|
|
||||||
|
const UN_SET: usize = 0;
|
||||||
|
const SETTING: usize = 1;
|
||||||
|
const SET: usize = 2;
|
||||||
|
|
||||||
|
static mut GLOBAL_REPORTER: Option<Report> = None;
|
||||||
|
|
||||||
/// Install this `Reporter` as the global default if one is
|
/// Install this `Reporter` as the global default if one is
|
||||||
/// not already set.
|
/// not already set.
|
||||||
///
|
///
|
||||||
|
@ -69,28 +104,70 @@ pub trait Reporter: 'static {
|
||||||
|
|
||||||
/// Invoked if the import couldn't be resolved
|
/// Invoked if the import couldn't be resolved
|
||||||
fn on_unresolved_import(&self, _import: &Path) {}
|
fn on_unresolved_import(&self, _import: &Path) {}
|
||||||
|
|
||||||
|
/// If `self` is the same type as the provided `TypeId`, returns an untyped
|
||||||
|
/// [`NonNull`] pointer to that type. Otherwise, returns `None`.
|
||||||
|
///
|
||||||
|
/// If you wish to downcast a `Reporter`, it is strongly advised to use
|
||||||
|
/// the safe API provided by [`downcast_ref`] instead.
|
||||||
|
///
|
||||||
|
/// This API is required for `downcast_raw` to be a trait method; a method
|
||||||
|
/// signature like [`downcast_ref`] (with a generic type parameter) is not
|
||||||
|
/// object-safe, and thus cannot be a trait method for `Reporter`. This
|
||||||
|
/// means that if we only exposed `downcast_ref`, `Reporter`
|
||||||
|
/// implementations could not override the downcasting behavior
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The [`downcast_ref`] method expects that the pointer returned by
|
||||||
|
/// `downcast_raw` points to a valid instance of the type
|
||||||
|
/// with the provided `TypeId`. Failure to ensure this will result in
|
||||||
|
/// undefined behaviour, so implementing `downcast_raw` is unsafe.
|
||||||
|
unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
|
||||||
|
if id == TypeId::of::<Self>() {
|
||||||
|
Some(NonNull::from(self).cast())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dyn Reporter {
|
||||||
|
/// Returns `true` if this `Reporter` is the same type as `T`.
|
||||||
|
pub fn is<T: Any>(&self) -> bool {
|
||||||
|
self.downcast_ref::<T>().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns some reference to this `Reporter` value if it is of type `T`,
|
||||||
|
/// or `None` if it isn't.
|
||||||
|
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
|
||||||
|
unsafe {
|
||||||
|
let raw = self.downcast_raw(TypeId::of::<T>())?;
|
||||||
|
Some(&*(raw.cast().as_ptr()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn solc_spawn(solc: &Solc, version: &Version, input: &CompilerInput) {
|
pub(crate) fn solc_spawn(solc: &Solc, version: &Version, input: &CompilerInput) {
|
||||||
with_global(|r| r.reporter.on_solc_spawn(solc, version, input));
|
get_default(|r| r.reporter.on_solc_spawn(solc, version, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn solc_success(solc: &Solc, version: &Version, output: &CompilerOutput) {
|
pub(crate) fn solc_success(solc: &Solc, version: &Version, output: &CompilerOutput) {
|
||||||
with_global(|r| r.reporter.on_solc_success(solc, version, output));
|
get_default(|r| r.reporter.on_solc_success(solc, version, output));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(crate) fn solc_installation_start(version: &Version) {
|
pub(crate) fn solc_installation_start(version: &Version) {
|
||||||
with_global(|r| r.reporter.on_solc_installation_start(version));
|
get_default(|r| r.reporter.on_solc_installation_start(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(crate) fn solc_installation_success(version: &Version) {
|
pub(crate) fn solc_installation_success(version: &Version) {
|
||||||
with_global(|r| r.reporter.on_solc_installation_success(version));
|
get_default(|r| r.reporter.on_solc_installation_success(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unresolved_import(import: &Path) {
|
pub(crate) fn unresolved_import(import: &Path) {
|
||||||
with_global(|r| r.reporter.on_unresolved_import(import));
|
get_default(|r| r.reporter.on_unresolved_import(import));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_global() -> Option<&'static Report> {
|
fn get_global() -> Option<&'static Report> {
|
||||||
|
@ -106,10 +183,97 @@ fn get_global() -> Option<&'static Report> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes a closure with a reference to this thread's current [reporter].
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_default<T, F>(mut f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut(&Report) -> T,
|
||||||
|
{
|
||||||
|
if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
|
||||||
|
// fast path if no scoped reporter has been set; use the global
|
||||||
|
// default.
|
||||||
|
return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
get_default_scoped(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn get_default_scoped<T, F>(mut f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut(&Report) -> T,
|
||||||
|
{
|
||||||
|
CURRENT_STATE
|
||||||
|
.try_with(|state| {
|
||||||
|
let scoped = state.scoped.borrow_mut();
|
||||||
|
f(&*scoped)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| f(&Report::none()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes a closure with a reference to the `Reporter`.
|
/// Executes a closure with a reference to the `Reporter`.
|
||||||
pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
|
pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
|
||||||
let dispatch = get_global()?;
|
let report = get_global()?;
|
||||||
Some(f(dispatch))
|
Some(f(report))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets this reporter as the scoped reporter for the duration of a closure.
|
||||||
|
pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
|
||||||
|
// When this guard is dropped, the scoped reporter will be reset to the
|
||||||
|
// prior reporter. Using this (rather than simply resetting after calling
|
||||||
|
// `f`) ensures that we always reset to the prior reporter even if `f`
|
||||||
|
// panics.
|
||||||
|
let _guard = set_scoped(report);
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The report state of a thread.
|
||||||
|
struct State {
|
||||||
|
/// This thread's current scoped reporter.
|
||||||
|
scoped: RefCell<Report>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Replaces the current scoped reporter on this thread with the provided
|
||||||
|
/// reporter.
|
||||||
|
///
|
||||||
|
/// Dropping the returned `ResetGuard` will reset the scoped reporter to
|
||||||
|
/// the previous value.
|
||||||
|
#[inline]
|
||||||
|
fn set_scoped(new_report: Report) -> ScopeGuard {
|
||||||
|
let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok();
|
||||||
|
EXISTS.store(true, Ordering::Release);
|
||||||
|
SCOPED_COUNT.fetch_add(1, Ordering::Release);
|
||||||
|
ScopeGuard(prior)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A guard that resets the current scoped reporter to the prior
|
||||||
|
/// scoped reporter when dropped.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScopeGuard(Option<Report>);
|
||||||
|
|
||||||
|
impl Drop for ScopeGuard {
|
||||||
|
#[inline]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
SCOPED_COUNT.fetch_sub(1, Ordering::Release);
|
||||||
|
if let Some(report) = self.0.take() {
|
||||||
|
// Replace the reporter and then drop the old one outside
|
||||||
|
// of the thread-local context.
|
||||||
|
let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
|
||||||
|
drop(prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the reporter as the scoped reporter for the duration of the lifetime
|
||||||
|
/// of the returned DefaultGuard
|
||||||
|
#[must_use = "Dropping the guard unregisters the reporter."]
|
||||||
|
pub fn set_scoped(reporter: &Report) -> ScopeGuard {
|
||||||
|
// When this guard is dropped, the scoped reporter will be reset to the
|
||||||
|
// prior default. Using this ensures that we always reset to the prior
|
||||||
|
// reporter even if the thread calling this function panics.
|
||||||
|
State::set_scoped(reporter.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A no-op [`Reporter`] that does nothing.
|
/// A no-op [`Reporter`] that does nothing.
|
||||||
|
@ -165,6 +329,7 @@ impl fmt::Display for SetGlobalReporterError {
|
||||||
impl Error for SetGlobalReporterError {}
|
impl Error for SetGlobalReporterError {}
|
||||||
|
|
||||||
/// `Report` trace data to a [`Reporter`].
|
/// `Report` trace data to a [`Reporter`].
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
reporter: Arc<dyn Reporter + Send + Sync>,
|
reporter: Arc<dyn Reporter + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
@ -184,16 +349,20 @@ impl Report {
|
||||||
{
|
{
|
||||||
Self { reporter: Arc::new(reporter) }
|
Self { reporter: Arc::new(reporter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if this `Report` forwards to a reporter of type
|
||||||
|
/// `T`.
|
||||||
|
#[inline]
|
||||||
|
pub fn is<T: Any>(&self) -> bool {
|
||||||
|
<dyn Reporter>::is::<T>(&*self.reporter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracks the state of `GLOBAL_REPORTER`
|
impl fmt::Debug for Report {
|
||||||
static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.pad("Report(...)")
|
||||||
const UN_SET: usize = 0;
|
}
|
||||||
const SETTING: usize = 1;
|
}
|
||||||
const SET: usize = 2;
|
|
||||||
|
|
||||||
static mut GLOBAL_REPORTER: Option<Report> = None;
|
|
||||||
|
|
||||||
/// Sets this report as the global default for the duration of the entire program.
|
/// Sets this report as the global default for the duration of the entire program.
|
||||||
///
|
///
|
||||||
|
@ -216,3 +385,35 @@ fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
|
||||||
Err(SetGlobalReporterError { _priv: () })
|
Err(SetGlobalReporterError { _priv: () })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scoped_reporter_works() {
|
||||||
|
struct TestReporter;
|
||||||
|
impl Reporter for TestReporter {}
|
||||||
|
|
||||||
|
with_scoped(&Report::new(TestReporter), || {
|
||||||
|
get_default(|reporter| assert!(reporter.is::<TestReporter>()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_and_scoped_reporter_works() {
|
||||||
|
get_default(|reporter| {
|
||||||
|
assert!(reporter.is::<NoReporter>());
|
||||||
|
});
|
||||||
|
|
||||||
|
set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap();
|
||||||
|
struct TestReporter;
|
||||||
|
impl Reporter for TestReporter {}
|
||||||
|
|
||||||
|
with_scoped(&Report::new(TestReporter), || {
|
||||||
|
get_default(|reporter| assert!(reporter.is::<TestReporter>()))
|
||||||
|
});
|
||||||
|
|
||||||
|
get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue