use std::sync::atomic::{AtomicI32, Ordering};
#[derive(Debug, Clone, PartialEq)]
enum Tok {
Pattern,
File,
Global,
Str {
text: String,
quote: Option<char>,
},
Equals,
Comma,
OBrace,
CBrace,
}
fn tok_text(tok: &Tok) -> String {
match tok {
Tok::Pattern => "pattern".into(),
Tok::File => "file".into(),
Tok::Global => "global".into(),
Tok::Str {
text,
quote: Some(q),
} => format!("{q}{text}{q}"),
Tok::Str { text, quote: None } => text.clone(),
Tok::Equals => "=".into(),
Tok::Comma => ",".into(),
Tok::OBrace => "{".into(),
Tok::CBrace => "}".into(),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubstitutionFault {
pub line: usize,
pub message: Option<String>,
pub yytext: String,
}
impl SubstitutionFault {
pub fn row_failed(load: &TemplateLoad) -> Self {
Self {
line: load.line,
message: Some("Error while reading included file".into()),
yytext: tok_text(&Tok::CBrace),
}
}
fn invalid_character(line: usize, c: char) -> Self {
Self {
line,
message: Some(format!("invalid character '{c}'")),
yytext: c.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SubstitutionEvent {
Load(TemplateLoad),
Fault(SubstitutionFault),
Notice(String),
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Substitutions {
pub events: Vec<SubstitutionEvent>,
pub stopped: Option<SubstitutionFault>,
}
fn lex(input: &str) -> Lexed {
let chars: Vec<char> = input.chars().collect();
let mut out: Vec<(Tok, usize)> = Vec::new();
let mut faults: Vec<(usize, SubstitutionFault)> = Vec::new();
let mut i = 0;
let mut line = 1usize;
let is_bareword = |c: char| {
c.is_ascii_alphanumeric()
|| matches!(
c,
'_' | '-' | '+' | ':' | '.' | '/' | '\\' | '[' | ']' | '<' | '>' | ';'
)
};
while i < chars.len() {
let c = chars[i];
match c {
'\n' => {
line += 1;
i += 1;
}
' ' | '\t' | '\r' => i += 1,
'#' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
}
'=' => {
out.push((Tok::Equals, line));
i += 1;
}
',' => {
out.push((Tok::Comma, line));
i += 1;
}
'{' => {
out.push((Tok::OBrace, line));
i += 1;
}
'}' => {
out.push((Tok::CBrace, line));
i += 1;
}
'"' | '\'' => {
match scan_quoted(&chars, i) {
Some((text, next)) => {
out.push((
Tok::Str {
text,
quote: Some(c),
},
line,
));
i = next;
}
None => {
faults.push((out.len(), SubstitutionFault::invalid_character(line, c)));
i += 1;
}
}
}
_ if is_bareword(c) => {
let mut s = String::new();
while i < chars.len() && is_bareword(chars[i]) {
s.push(chars[i]);
i += 1;
}
let tok = match s.as_str() {
"pattern" => Tok::Pattern,
"file" => Tok::File,
"global" => Tok::Global,
_ => Tok::Str {
text: s,
quote: None,
},
};
out.push((tok, line));
}
other => {
faults.push((out.len(), SubstitutionFault::invalid_character(line, other)));
i += 1;
}
}
}
Lexed {
toks: out,
faults,
final_line: line,
}
}
struct Lexed {
toks: Vec<(Tok, usize)>,
faults: Vec<(usize, SubstitutionFault)>,
final_line: usize,
}
fn scan_quoted(chars: &[char], at: usize) -> Option<(String, usize)> {
let quote = chars[at];
let mut text = String::new();
let mut i = at + 1;
loop {
let ch = *chars.get(i)?;
if ch == quote {
return Some((text, i + 1));
}
if ch == '\n' {
return None;
}
if ch == '\\' {
let next = *chars.get(i + 1)?;
if next == '\n' {
return None;
}
text.push('\\');
text.push(next);
i += 2;
continue;
}
text.push(ch);
i += 1;
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TemplateLoad {
pub file: String,
pub macros: Vec<RowMacro>,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RowMacro {
pub name: String,
pub value: String,
pub quoted: bool,
}
static DB_TEMPLATE_MAX_VARS: AtomicI32 = AtomicI32::new(100);
pub fn db_template_max_vars() -> i32 {
DB_TEMPLATE_MAX_VARS.load(Ordering::Relaxed)
}
pub fn set_db_template_max_vars(value: i32) {
DB_TEMPLATE_MAX_VARS.store(value, Ordering::Relaxed);
}
struct RowValue {
text: String,
quoted: bool,
}
struct Parser {
toks: Vec<(Tok, usize)>,
pos: usize,
final_line: usize,
globals: Vec<RowMacro>,
events: Vec<(usize, SubstitutionEvent)>,
load_count: usize,
}
type ParseResult<T> = Result<T, SubstitutionFault>;
impl Parser {
fn new(toks: Vec<(Tok, usize)>, final_line: usize) -> Self {
Self {
toks,
pos: 0,
final_line,
globals: Vec::new(),
events: Vec::new(),
load_count: 0,
}
}
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.pos).map(|(t, _)| t)
}
fn line(&self) -> usize {
self.toks
.get(self.pos)
.map(|(_, l)| *l)
.unwrap_or(self.final_line)
}
fn syntax_error(&self) -> SubstitutionFault {
SubstitutionFault {
line: self.line(),
message: Some("syntax error".into()),
yytext: self.peek().map(tok_text).unwrap_or_default(),
}
}
fn err_unnamed(&self) -> SubstitutionFault {
SubstitutionFault {
line: self.line(),
message: None,
yytext: self.peek().map(tok_text).unwrap_or_default(),
}
}
fn next(&mut self) -> Option<Tok> {
let t = self.toks.get(self.pos).map(|(t, _)| t.clone());
if t.is_some() {
self.pos += 1;
}
t
}
fn expect(&mut self, want: &Tok) -> ParseResult<()> {
match self.peek() {
Some(got) if got == want => {
self.pos += 1;
Ok(())
}
_ => Err(self.syntax_error()),
}
}
fn expect_str(&mut self) -> ParseResult<String> {
Ok(self.expect_value()?.text)
}
fn expect_word(&mut self) -> ParseResult<String> {
if let Some(Tok::Str { quote: Some(_), .. }) = self.peek() {
return Err(self.syntax_error());
}
self.expect_str()
}
fn expect_value(&mut self) -> ParseResult<RowValue> {
match self.peek() {
Some(Tok::Str { text, quote }) => {
let value = RowValue {
text: text.clone(),
quoted: quote.is_some(),
};
self.pos += 1;
Ok(value)
}
_ => Err(self.syntax_error()),
}
}
fn parse(&mut self) -> ParseResult<()> {
while let Some(tok) = self.peek() {
match tok {
Tok::Global => self.parse_global()?,
Tok::File => self.parse_file()?,
_ => return Err(self.syntax_error()),
}
}
Ok(())
}
fn parse_global(&mut self) -> ParseResult<()> {
self.expect(&Tok::Global)?;
self.expect(&Tok::OBrace)?;
let defs = self.parse_variable_definitions()?;
self.expect(&Tok::CBrace)?;
self.globals.extend(defs);
Ok(())
}
fn parse_file(&mut self) -> ParseResult<()> {
let entry_line = self.line();
self.expect(&Tok::File)?;
let filename = self.expect_str()?;
let loads_before = self.load_count;
self.expect(&Tok::OBrace)?;
if self.peek() == Some(&Tok::CBrace) {
self.next();
} else {
match self.peek() {
Some(Tok::Pattern) => self.parse_pattern_block(&filename)?,
_ => self.parse_variable_substitutions(&filename)?,
}
self.expect(&Tok::CBrace)?;
}
if self.load_count == loads_before {
tracing::warn!(
file = %filename,
line = entry_line,
"substitutions entry produced no template loads"
);
}
Ok(())
}
fn parse_pattern_block(&mut self, filename: &str) -> ParseResult<()> {
self.expect(&Tok::Pattern)?;
self.expect(&Tok::OBrace)?;
let mut names: Vec<String> = Vec::new();
while self.peek() != Some(&Tok::CBrace) {
match self.peek() {
Some(Tok::Comma) => {
self.next();
}
Some(Tok::Str { .. }) => {
let ceiling = db_template_max_vars();
if names.len() as i32 >= ceiling {
let fault = self.err_unnamed();
self.notice(format!(
"More than dbTemplateMaxVars = {ceiling} macro variables used"
));
self.events
.push((self.pos, SubstitutionEvent::Fault(fault)));
self.next();
continue;
}
names.push(self.expect_word()?)
}
_ => return Err(self.syntax_error()),
}
}
self.expect(&Tok::CBrace)?;
while let Some(tok) = self.peek() {
match tok {
Tok::Global => self.parse_global()?,
Tok::OBrace => {
let row = self.parse_pattern_row()?;
let macros = self.pattern_macros(&names, &row);
self.emit_load(filename, macros);
}
Tok::Str { .. } => {
let extraneous = self.expect_word()?;
let row = self.parse_pattern_row()?;
let macros = self.pattern_macros(&names, &row);
self.deprecated_row_notice(&extraneous);
self.emit_load(filename, macros);
}
Tok::CBrace => break,
_ => return Err(self.syntax_error()),
}
}
Ok(())
}
fn parse_pattern_row(&mut self) -> ParseResult<Vec<(RowValue, usize)>> {
self.expect(&Tok::OBrace)?;
let mut values: Vec<(RowValue, usize)> = Vec::new();
while self.peek() != Some(&Tok::CBrace) {
match self.peek() {
Some(Tok::Comma) => {
self.next();
}
Some(Tok::Str { .. }) => {
let line = self.line();
values.push((self.expect_value()?, line));
}
_ => return Err(self.syntax_error()),
}
}
self.expect(&Tok::CBrace)?;
Ok(values)
}
fn pattern_macros(&mut self, names: &[String], row: &[(RowValue, usize)]) -> Vec<RowMacro> {
for (_, line) in row.iter().skip(names.len()) {
self.notice(format!(
"dbLoadTemplate: Too many values given, line {line}."
));
}
names
.iter()
.zip(row.iter())
.map(|(name, (value, _))| RowMacro {
name: name.clone(),
value: value.text.clone(),
quoted: value.quoted,
})
.collect()
}
fn parse_variable_substitutions(&mut self, filename: &str) -> ParseResult<()> {
while let Some(tok) = self.peek() {
match tok {
Tok::Global => self.parse_global()?,
Tok::OBrace => {
self.next();
let defs = self.parse_variable_definitions()?;
self.expect(&Tok::CBrace)?;
self.emit_load(filename, defs);
}
Tok::Str { .. } => {
let extraneous = self.expect_word()?;
self.expect(&Tok::OBrace)?;
let defs = self.parse_variable_definitions()?;
self.expect(&Tok::CBrace)?;
self.deprecated_row_notice(&extraneous);
self.emit_load(filename, defs);
}
Tok::CBrace => break,
_ => return Err(self.syntax_error()),
}
}
Ok(())
}
fn parse_variable_definitions(&mut self) -> ParseResult<Vec<RowMacro>> {
let mut defs: Vec<RowMacro> = Vec::new();
while self.peek() != Some(&Tok::CBrace) {
match self.peek() {
Some(Tok::Comma) => {
self.next();
}
Some(Tok::Str { .. }) => {
let name = self.expect_word()?;
self.expect(&Tok::Equals)?;
let value = self.expect_value()?;
defs.push(RowMacro {
name,
value: value.text,
quoted: value.quoted,
});
}
_ => return Err(self.syntax_error()),
}
}
Ok(defs)
}
fn emit_load(&mut self, filename: &str, row: Vec<RowMacro>) {
let mut macros = self.globals.clone();
macros.extend(row);
self.load_count += 1;
self.events.push((
self.pos,
SubstitutionEvent::Load(TemplateLoad {
file: filename.to_string(),
macros,
line: self.last_line(),
}),
));
}
fn notice(&mut self, text: String) {
self.events
.push((self.pos, SubstitutionEvent::Notice(text)));
}
fn last_line(&self) -> usize {
self.toks[self.pos - 1].1
}
fn deprecated_row_notice(&mut self, word: &str) {
let line = self.last_line();
self.notice(format!(
"dbLoadTemplate: Substitution file uses deprecated syntax.\n \
the string '{word}' on line {line} that comes just before the\n \
'{{' character is extraneous and should be removed."
));
}
}
pub fn parse_substitutions(input: &str) -> Substitutions {
let Lexed {
toks,
faults,
final_line,
} = lex(input);
let mut parser = Parser::new(toks, final_line);
let stopped = parser.parse().err();
let last_read = parser.pos;
let mut faults = faults
.into_iter()
.filter(|(at, _)| *at <= last_read)
.peekable();
let mut events = Vec::new();
for (at, event) in std::mem::take(&mut parser.events) {
while faults.peek().is_some_and(|(fault_at, _)| *fault_at < at) {
events.push(SubstitutionEvent::Fault(faults.next().unwrap().1));
}
events.push(event);
}
events.extend(faults.map(|(_, fault)| SubstitutionEvent::Fault(fault)));
Substitutions { events, stopped }
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::Path;
use super::super::include::{DbLoadConfig, parse_db_opened_with_breaktables};
use super::*;
fn pairs(macros: &[RowMacro]) -> Vec<(String, String)> {
macros
.iter()
.map(|m| (m.name.clone(), m.value.clone()))
.collect()
}
fn loads_of_events(subs: &Substitutions) -> Vec<&TemplateLoad> {
subs.events
.iter()
.filter_map(|ev| match ev {
SubstitutionEvent::Load(load) => Some(load),
_ => None,
})
.collect()
}
fn loads_of(src: &str) -> Vec<TemplateLoad> {
let subs = parse_substitutions(src);
assert_eq!(subs.stopped, None, "unexpected parse fault");
subs.events
.into_iter()
.map(|ev| match ev {
SubstitutionEvent::Load(load) => load,
other => panic!("unexpected {other:?}"),
})
.collect()
}
fn load_rows(
subs: &Path,
macros: &HashMap<String, String>,
config: &DbLoadConfig,
) -> Vec<super::super::DbRecordDef> {
let text = std::fs::read_to_string(subs).unwrap();
loads_of(&text)
.into_iter()
.flat_map(|load| {
let mut merged = macros.clone();
merged.extend(pairs(&load.macros));
let template =
super::super::include::db_open_file_located(&load.file, &config.include_paths)
.unwrap();
parse_db_opened_with_breaktables(&template, &merged, config)
.unwrap()
.records
})
.collect()
}
#[test]
fn parse_pattern_substitution() {
let src = r#"
file "rec.db" {
pattern { P, N }
{ "IOC:", "1" }
{ "IOC:", "2" }
}
"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 2);
assert_eq!(loads[0].file, "rec.db");
assert_eq!(
pairs(&loads[0].macros),
vec![("P".into(), "IOC:".into()), ("N".into(), "1".into())]
);
assert_eq!(
pairs(&loads[1].macros),
vec![("P".into(), "IOC:".into()), ("N".into(), "2".into())]
);
}
#[test]
fn parse_pattern_with_comma_separated_names() {
let src = r#"file "x.db" { pattern {A,B,C} {"1","2","3"} }"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 1);
assert_eq!(
pairs(&loads[0].macros),
vec![
("A".into(), "1".into()),
("B".into(), "2".into()),
("C".into(), "3".into())
]
);
}
#[test]
fn parse_variable_substitution() {
let src = r#"
file "rec.db" {
{ A=1, B=2 }
{ A=3, B=4 }
}
"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 2);
assert_eq!(
pairs(&loads[0].macros),
vec![("A".into(), "1".into()), ("B".into(), "2".into())]
);
assert_eq!(
pairs(&loads[1].macros),
vec![("A".into(), "3".into()), ("B".into(), "4".into())]
);
}
#[test]
fn parse_global_block_applies_to_all_rows() {
let src = r#"
global { G="gval" }
file "rec.db" {
pattern { N }
{ "1" }
{ "2" }
}
"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 2);
assert_eq!(
pairs(&loads[0].macros),
vec![("G".into(), "gval".into()), ("N".into(), "1".into())]
);
assert_eq!(
pairs(&loads[1].macros),
vec![("G".into(), "gval".into()), ("N".into(), "2".into())]
);
}
#[test]
fn parse_quoted_filename() {
let src = r#"file "path/to/rec.db" { { A=1 } }"#;
let loads = loads_of(src);
assert_eq!(loads[0].file, "path/to/rec.db");
}
#[test]
fn parse_bare_filename() {
let src = r#"file rec.db { { A=1 } }"#;
let loads = loads_of(src);
assert_eq!(loads[0].file, "rec.db");
}
#[test]
fn parse_empty_file_body() {
let src = r#"file "rec.db" { }"#;
let loads = loads_of(src);
assert!(loads.is_empty());
}
fn captured(src: &str) -> String {
use std::sync::{Arc, Mutex};
use tracing_subscriber::fmt::MakeWriter;
#[derive(Clone, Default)]
struct Buf(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Buf {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for Buf {
type Writer = Buf;
fn make_writer(&'a self) -> Buf {
self.clone()
}
}
let buf = Buf::default();
let subscriber = tracing_subscriber::fmt()
.with_writer(buf.clone())
.with_max_level(tracing::Level::WARN)
.finish();
let _guard = tracing::subscriber::set_default(subscriber);
parse_substitutions(src);
String::from_utf8_lossy(&buf.0.lock().unwrap()).into_owned()
}
#[test]
fn zero_load_file_entry_warns() {
for src in [
r#"file "rec.db" { }"#,
r#"file "rec.db" { pattern {N} }"#,
r#"file "rec.db" { global {A=1} }"#,
] {
let out = captured(src);
assert!(
out.contains("no template loads") && out.contains("rec.db"),
"row-less entry {src:?} must warn, got: {out:?}"
);
}
assert_eq!(
captured(r#"file "rec.db" { { A=1 } }"#),
"",
"an entry with rows must not warn"
);
}
fn notices_of(subs: &Substitutions) -> Vec<&str> {
subs.events
.iter()
.filter_map(|ev| match ev {
SubstitutionEvent::Notice(text) => Some(text.as_str()),
_ => None,
})
.collect()
}
#[test]
fn a_value_past_the_last_pattern_name_is_reported() {
let subs = parse_substitutions(
"file \"rec.db\" {\n pattern { A, B }\n { \"1\", \"2\", \"3\" }\n}",
);
let out = notices_of(&subs).join("\n");
assert!(
out.contains("Too many values given, line 3."),
"the surplus value must be reported with its line, got: {out:?}"
);
assert_eq!(
out.matches("Too many values given").count(),
1,
"one report per surplus value, got: {out:?}"
);
let loads = loads_of_events(&subs);
assert_eq!(
pairs(&loads[0].macros),
vec![
("A".to_string(), "1".to_string()),
("B".to_string(), "2".to_string())
],
"the surplus value is dropped, the rest still bind"
);
let short = parse_substitutions("file \"rec.db\" {\n pattern { A, B }\n { \"1\" }\n}");
assert!(
notices_of(&short).is_empty(),
"a short row leaves B unbound without a diagnostic, as in C"
);
}
#[test]
fn parse_empty_pattern_row() {
let src = r#"file "rec.db" { pattern {N} {} }"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 1);
assert!(loads[0].macros.is_empty());
}
#[test]
fn parse_comments_and_whitespace() {
let src = r#"
# header comment
file "rec.db" { # trailing comment
pattern { N }
{ "1" } # row comment
}
"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 1);
}
#[test]
fn parse_deprecated_word_prefix_row() {
let src = r#"file "rec.db" { pattern {N} extra {"1"} }"#;
let subs = parse_substitutions(src);
let loads = loads_of_events(&subs);
assert_eq!(loads.len(), 1);
assert_eq!(pairs(&loads[0].macros), vec![("N".into(), "1".into())]);
assert_eq!(
notices_of(&subs),
vec![
"dbLoadTemplate: Substitution file uses deprecated syntax.\n \
the string 'extra' on line 1 that comes just before the\n \
'{' character is extraneous and should be removed."
]
);
}
#[test]
fn parse_multiple_files() {
let src = r#"
file "a.db" { { X=1 } }
file "b.db" { { Y=2 } }
"#;
let loads = loads_of(src);
assert_eq!(loads.len(), 2);
assert_eq!(loads[0].file, "a.db");
assert_eq!(loads[1].file, "b.db");
}
fn faults_of(subs: &Substitutions) -> Vec<&SubstitutionFault> {
subs.events
.iter()
.filter_map(|ev| match ev {
SubstitutionEvent::Fault(f) => Some(f),
_ => None,
})
.collect()
}
#[test]
fn an_unterminated_string_degrades_to_one_bad_character() {
let src = "file \"rec.db\" { { N=\"oops\nB=1 } }";
let subs = parse_substitutions(src);
assert_eq!(subs.stopped, None);
assert_eq!(
faults_of(&subs),
vec![&SubstitutionFault {
line: 1,
message: Some("invalid character '\"'".into()),
yytext: "\"".into(),
}]
);
let loads = loads_of_events(&subs);
assert_eq!(loads.len(), 1);
assert_eq!(
pairs(&loads[0].macros),
vec![("N".into(), "oops".into()), ("B".into(), "1".into())]
);
}
#[test]
fn a_backslash_before_a_newline_does_not_continue_the_string() {
let src = "file \"rec.db\" { { N=\"oops\\\nB\" } }";
let subs = parse_substitutions(src);
assert_eq!(
faults_of(&subs)
.iter()
.map(|f| (f.line, f.message.clone().unwrap_or_default()))
.collect::<Vec<_>>(),
vec![
(1, "invalid character '\"'".to_string()),
(2, "invalid character '\"'".to_string()),
]
);
assert!(loads_of_events(&subs).is_empty());
let stopped = subs
.stopped
.as_ref()
.expect("the dangling name must stop the parse");
assert_eq!((stopped.line, stopped.yytext.as_str()), (2, "}"));
}
#[test]
fn parse_rejects_missing_keyword() {
let src = r#"{ A=1 }"#;
let stopped = parse_substitutions(src).stopped.expect("must stop");
assert_eq!((stopped.line, stopped.yytext.as_str()), (1, "{"));
}
#[test]
fn template_loads_drive_one_dbloadrecords_per_row() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let tmpl = dir.path().join("rec.db");
let mut f = std::fs::File::create(&tmpl).unwrap();
writeln!(f, r#"record(ai, "$(P)$(N)") {{ field(VAL, "$(N)") }}"#).unwrap();
let subs = dir.path().join("test.substitutions");
let mut f = std::fs::File::create(&subs).unwrap();
writeln!(f, r#"global {{ P="IOC:" }}"#).unwrap();
writeln!(f, r#"file "rec.db" {{"#).unwrap();
writeln!(f, r#" pattern {{ N }}"#).unwrap();
writeln!(f, r#" {{ "1" }}"#).unwrap();
writeln!(f, r#" {{ "2" }}"#).unwrap();
writeln!(f, r#"}}"#).unwrap();
let config = DbLoadConfig {
include_paths: vec![dir.path().to_path_buf()],
max_include_depth: 32,
};
let recs = load_rows(&subs, &HashMap::new(), &config);
assert_eq!(recs.len(), 2);
assert_eq!(recs[0].name, "IOC:1");
assert_eq!(recs[0].fields[0].value, "1");
assert_eq!(recs[1].name, "IOC:2");
assert_eq!(recs[1].fields[0].value, "2");
}
#[test]
fn template_loads_take_templates_from_the_path_list() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path_dir = tempfile::tempdir().unwrap();
let mut f = std::fs::File::create(path_dir.path().join("rec.db")).unwrap();
writeln!(f, r#"record(ai, "FROM_PATH") {{ }}"#).unwrap();
let mut f = std::fs::File::create(dir.path().join("rec.db")).unwrap();
writeln!(f, r#"record(ai, "FROM_SUBS_DIR") {{ }}"#).unwrap();
let subs = dir.path().join("t.substitutions");
let mut f = std::fs::File::create(&subs).unwrap();
writeln!(f, r#"file "rec.db" {{ {{ }} }}"#).unwrap();
let config = DbLoadConfig {
include_paths: vec![path_dir.path().to_path_buf()],
max_include_depth: 32,
};
let recs = load_rows(&subs, &HashMap::new(), &config);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].name, "FROM_PATH");
}
#[test]
#[serial_test::serial(epics_env)]
fn template_loads_env_expand_the_template_name() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let tpl_dir = tempfile::tempdir().unwrap();
let key = "EPICS_RS_TEST_SUBS_TOP";
let mut f = std::fs::File::create(tpl_dir.path().join("t.template")).unwrap();
writeln!(f, r#"record(ai, "FROM_ENV") {{ }}"#).unwrap();
let subs = dir.path().join("t.substitutions");
let mut f = std::fs::File::create(&subs).unwrap();
writeln!(f, r#"file "$({key})/t.template" {{ {{ }} }}"#).unwrap();
unsafe { std::env::set_var(key, super::super::macro_safe_path(tpl_dir.path())) };
let recs = load_rows(&subs, &HashMap::new(), &DbLoadConfig::default());
unsafe { std::env::remove_var(key) };
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].name, "FROM_ENV");
}
#[test]
fn template_loads_let_the_row_override_caller_macros() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let tmpl = dir.path().join("rec.db");
let mut f = std::fs::File::create(&tmpl).unwrap();
writeln!(f, r#"record(ai, "$(N)") {{ field(VAL, "0") }}"#).unwrap();
let subs = dir.path().join("v.substitutions");
let mut f = std::fs::File::create(&subs).unwrap();
writeln!(f, r#"file "rec.db" {{ {{ N=ROW }} }}"#).unwrap();
let mut macros = HashMap::new();
macros.insert("N".to_string(), "CALLER".to_string());
let config = DbLoadConfig {
include_paths: vec![dir.path().to_path_buf()],
max_include_depth: 32,
};
let recs = load_rows(&subs, ¯os, &config);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].name, "ROW");
}
}