monument_cli/
lib.rs

1//! Crate for loading and running Monument's input files.  The CLI itself is a very thin wrapper
2//! around `monument_toml`, parsing CLI args and immediately calling into this.  This crate is also
3//! shared between the various integration test runners, making sure that the integration tests run
4//! in exactly the same way as Monument itself.
5
6#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
7
8pub mod args;
9pub mod calls;
10pub mod logging;
11pub mod music;
12pub mod toml_file;
13pub mod utils;
14
15use std::{
16    path::Path,
17    str::FromStr,
18    sync::{
19        atomic::{AtomicBool, Ordering},
20        Arc,
21    },
22    time::{Duration, Instant},
23};
24
25use log::LevelFilter;
26use monument::{Composition, Search};
27use ordered_float::OrderedFloat;
28use ringing_utils::PrettyDuration;
29use simple_logger::SimpleLogger;
30use toml_file::TomlFile;
31
32use crate::logging::{CompositionPrinter, SingleLineProgressLogger};
33
34pub fn init_logging(filter: LevelFilter) {
35    SimpleLogger::new()
36        .without_timestamps()
37        .with_colors(true)
38        .with_level(filter)
39        .init()
40        .unwrap();
41}
42
43pub fn run(
44    toml_path: &Path,
45    options: &args::Options,
46    env: Environment,
47) -> anyhow::Result<Option<SearchResult>> {
48    /// If the user specifies a [`DebugPrint`] flag with e.g. `-D layout`, then debug print the
49    /// corresponding value and exit.
50    macro_rules! debug_print {
51        ($variant: ident, $val: expr) => {
52            if options.debug_option == Some(DebugOption::$variant) {
53                dbg!($val);
54                return Ok(None);
55            }
56        };
57    }
58
59    let start_time = Instant::now();
60
61    // Generate & debug print the TOML file specifying the search
62    let toml_file = TomlFile::new(toml_path)?;
63    debug_print!(Toml, toml_file);
64    // If running in CLI mode, don't `drop` any of the search data structures, since Monument will
65    // exit shortly after the search terminates.  With the `Arc`-based data structures, this is
66    // seriously beneficial - it shaves many seconds off Monument's total running time.
67    let leak_search_memory = env == Environment::Cli;
68    // Convert the `TomlFile` into a `Layout` and other data required for running a search
69    let (params, music_displays) = toml_file.to_params(toml_path)?;
70    debug_print!(Params, params);
71    // Build the search
72    let search = Arc::new(Search::new(
73        params.clone(),
74        toml_file.config(options, leak_search_memory),
75    )?);
76    debug_print!(Search, search);
77
78    // Build all the data structures for the search
79    let comp_printer = CompositionPrinter::new(
80        music_displays,
81        search.clone(),
82        toml_file.should_print_atw(),
83        !options.dont_display_comp_numbers,
84    );
85    let mut update_logger = SingleLineProgressLogger::new(match options.only_display_update_line {
86        true => None,
87        false => Some(comp_printer.clone()),
88    });
89
90    if options.debug_option == Some(DebugOption::StopBeforeSearch) {
91        return Ok(None);
92    }
93
94    // In CLI mode, attach `ctrl-C` to the abort flag
95    let abort_flag = Arc::new(AtomicBool::new(false));
96    if env == Environment::Cli {
97        let abort_flag = Arc::clone(&abort_flag);
98        if let Err(e) = ctrlc::set_handler(move || abort_flag.store(true, Ordering::SeqCst)) {
99            log::warn!("Error setting ctrl-C handler: {}", e);
100        }
101    }
102
103    // Run the search, collecting the compositions as the search runs
104    let mut comps = Vec::<(Composition, usize)>::new();
105    search.run(
106        |update| {
107            let next_comp_number = comps.len();
108            if let Some(comp) = update_logger.log(update, next_comp_number) {
109                comps.push((comp, next_comp_number));
110            }
111        },
112        &abort_flag,
113    );
114
115    // Once the search has completed, sort the compositions and return
116    fn rounded_float(f: f32) -> OrderedFloat<f32> {
117        const FACTOR: f32 = 1e-6;
118        let rounded = (f / FACTOR).round() * FACTOR;
119        OrderedFloat(rounded)
120    }
121    comps.sort_by_cached_key(|(comp, _generation_index)| {
122        (
123            rounded_float(comp.music_score(&params)),
124            rounded_float(comp.average_score()),
125            comp.call_string(&params),
126        )
127    });
128    Ok(Some(SearchResult {
129        comps,
130        comp_printer,
131        duration: start_time.elapsed(),
132        aborted: abort_flag.load(Ordering::SeqCst),
133
134        search,
135    }))
136}
137
138/// How this instance of Monument is being run
139#[derive(Debug, PartialEq, Eq)]
140pub enum Environment {
141    /// Being run by the test harness as a test case
142    TestHarness,
143    /// Being run by the CLI
144    Cli,
145}
146
147#[derive(Debug, Clone)]
148pub struct SearchResult {
149    pub comps: Vec<(Composition, usize)>,
150    pub search: Arc<Search>,
151    pub duration: Duration,
152    pub aborted: bool,
153
154    comp_printer: self::logging::CompositionPrinter,
155}
156
157impl SearchResult {
158    pub fn print(&mut self) {
159        eprintln!("\n\n\n\nSEARCH COMPLETE!\n\n\n");
160        for (c, generation_index) in &self.comps {
161            println!(
162                "{}",
163                self.comp_printer
164                    .comp_string_with_possible_headers(c, *generation_index)
165            );
166        }
167        println!("{}", self.comp_printer.footer_lines());
168        eprintln!(
169            "{} composition{} generated{} {}",
170            self.comps.len(),
171            if self.comps.len() == 1 { "" } else { "s" }, // Handle "1 composition"
172            match self.aborted {
173                true => ", aborted after",
174                false => " in",
175            },
176            PrettyDuration(self.duration)
177        );
178    }
179}
180
181/// What item should be debug printed
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum DebugOption {
184    Toml,
185    Params,
186    Search,
187    Graph,
188    /// Stop just before the search starts, to let the user see what's been printed out without
189    /// scrolling
190    StopBeforeSearch,
191}
192
193impl FromStr for DebugOption {
194    type Err = String;
195
196    fn from_str(v: &str) -> Result<Self, String> {
197        Ok(match v.to_lowercase().as_str() {
198            "toml" => Self::Toml,
199            "params" => Self::Params,
200            "search" => Self::Search,
201            "graph" => Self::Graph,
202            "no-search" => Self::StopBeforeSearch,
203            #[rustfmt::skip] // See https://github.com/rust-lang/rustfmt/issues/5204
204            _ => return Err(format!(
205                "Unknown value {:?}. Expected `toml`, `params`, `search`, `graph` or `no-search`.",
206                v
207            )),
208        })
209    }
210}