Skip to main content

gpiosim/
lib.rs

1// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! A library for creating and controlling GPIO simulators for testing users of
6//! the Linux GPIO uAPI (both v1 and v2).
7//!
8//! The simulators are provided by the Linux **gpio-sim** kernel module and require a
9//! recent kernel (v5.19 or later) built with **CONFIG_GPIO_SIM**.
10//!
11//! Simulators ([`Sim`]) contain one or more chips, each with a collection of lines being
12//! simulated. The [`Builder`] is responsible for constructing the [`Sim`] and taking it live.
13//! Configuring a simulator involves adding [`Bank`]s, representing the
14//! chip, to the [`Builder`].
15//!
16//! Once live, the [`Chip`] exposes lines which may be manipulated to drive the
17//! GPIO uAPI from the kernel side.
18//! For input lines, applying a pull using [`Chip.set_pull`] and related
19//! methods controls the level of the simulated line.  For output lines,
20//! [`Chip.get_level`] returns the level the simulated line is being driven to.
21//!
22//! For simple tests that only require lines on a single chip, the [`Simpleton`]
23//! provides a simplified interface.
24//!
25//! Configuring a simulator involves *configfs*, and manipulating the chips once live
26//! involves *sysfs*, so root permissions are typically required to run a simulator.
27//!
28//! ## Example Usage
29//!
30//! Creating a simulator with two chips, with 8 and 42 lines respectively, each with
31//! several named lines and a hogged line:
32//!
33//! ```no_run
34//! # use gpiosim::Result;
35//! # fn main() -> Result<()> {
36//! use gpiosim::{Bank, Direction, Level};
37//!
38//! let sim = gpiosim::builder()
39//!     .with_name("some unique name")
40//!     .with_bank(
41//!         Bank::new(8, "left")
42//!             .name(3, "LED0")
43//!             .name(5, "BUTTON1")
44//!             .hog(2, "hogster", Direction::OutputLow)
45//!         )
46//!     .with_bank(
47//!         Bank::new(42, "right")
48//!             .name(3, "BUTTON2")
49//!             .name(4, "LED2")
50//!             .hog(7, "hogster", Direction::OutputHigh),
51//!     )
52//!     .live()?;
53//!
54//! let chips = sim.chips();
55//! let c = &chips[0];
56//! c.set_pull(5, Level::High);
57//! let level = c.get_level(3)?;
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! Use a [`Simpleton`] to create a single chip simulator with 12 lines, for where multiple chips or
63//! named lines are not required:
64//!
65//! ```no_run
66//! # use gpiosim::Result;
67//! # fn main() -> Result<()> {
68//! use gpiosim::{Level, Simpleton};
69//!
70//! let s = Simpleton::new(12);
71//! let c = s.chip();
72//! c.set_pull(5, Level::High);
73//! c.set_pull(6, Level::Low);
74//! let level = c.get_level(3)?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! [`Chip.set_pull`]: struct.Chip.html#method.set_pull
80//! [`Chip.get_level`]: struct.Chip.html#method.get_level
81
82use std::collections::HashMap;
83use std::env;
84use std::ffi::OsString;
85use std::fs::{self, File};
86use std::hash::{BuildHasherDefault, Hasher};
87use std::io::prelude::*;
88use std::io::BufReader;
89use std::os::unix::ffi::OsStringExt;
90use std::path::{Path, PathBuf};
91use std::process;
92use std::sync::atomic::{AtomicU32, Ordering};
93use std::thread::sleep;
94use std::time::Duration;
95
96/// A live simulator of one or more chips.
97#[derive(Debug, Eq, PartialEq)]
98pub struct Sim {
99    /// The name of the simulator in configfs and sysfs space.
100    name: String,
101
102    /// The details of the chips being simulated.
103    chips: Vec<Chip>,
104
105    /// Path to the gpio-sim in configfs.
106    dir: PathBuf,
107}
108
109impl Sim {
110    /// The details of the chips being simulated.
111    pub fn chips(&self) -> &[Chip] {
112        self.chips.as_slice()
113    }
114
115    /// The name of the simulator in configfs and sysfs space.
116    pub fn name(&self) -> &str {
117        &self.name
118    }
119
120    fn live(&mut self) -> Result<()> {
121        self.setup_configfs()?;
122        write_attr(&self.dir, "live", "1")?;
123        self.read_attrs()
124    }
125
126    fn cleanup_configfs(&mut self) {
127        if !self.dir.exists() {
128            return;
129        }
130        let _ = write_attr(&self.dir, "live", "0");
131        for (i, c) in self.chips.iter().enumerate() {
132            let bank = format!("bank{i}");
133            let bank_dir = self.dir.join(bank);
134            if !bank_dir.exists() {
135                continue;
136            }
137            for offset in c.cfg.hogs.keys() {
138                let line_dir = bank_dir.join(format!("line{offset}"));
139                let hog_dir = line_dir.join("hog");
140                let _ = fs::remove_dir(hog_dir);
141                let _ = fs::remove_dir(line_dir);
142            }
143            for offset in c.cfg.names.keys() {
144                let line_dir = bank_dir.join(format!("line{offset}"));
145                let _ = fs::remove_dir(line_dir);
146            }
147            let _ = fs::remove_dir(bank_dir);
148        }
149        let _ = fs::remove_dir(&self.dir);
150        while self.dir.exists() {}
151    }
152
153    fn setup_configfs(&mut self) -> Result<()> {
154        for (i, c) in self.chips.iter().enumerate() {
155            let bank_dir = self.dir.join(format!("bank{i}"));
156            fs::create_dir(&bank_dir)?;
157            write_attr(&bank_dir, "label", c.cfg.label.as_bytes())?;
158            write_attr(&bank_dir, "num_lines", format!("{}", c.cfg.num_lines))?;
159
160            for (offset, name) in &c.cfg.names {
161                let line_dir = bank_dir.join(format!("line{offset}"));
162                fs::create_dir(&line_dir)?;
163                write_attr(&line_dir, "name", name.as_bytes())?;
164            }
165            for (offset, hog) in &c.cfg.hogs {
166                let line_dir = bank_dir.join(format!("line{offset}"));
167                if !line_dir.exists() {
168                    fs::create_dir(&line_dir)?;
169                }
170                let hog_dir = line_dir.join("hog");
171                fs::create_dir(&hog_dir)?;
172                write_attr(&hog_dir, "name", hog.consumer.as_bytes())?;
173                write_attr(&hog_dir, "direction", hog.direction.as_str())?;
174            }
175        }
176        Ok(())
177    }
178
179    fn read_attrs(&mut self) -> Result<()> {
180        let dev_name = read_attr(&self.dir, "dev_name")?;
181        for (i, c) in self.chips.iter_mut().enumerate() {
182            let bank_dir = self.dir.join(format!("bank{i}"));
183            let chip_name = read_attr(&bank_dir, "chip_name")?;
184            c.dev_path = "/dev".into();
185            c.dev_path.push(&chip_name);
186            // Flag anyone (e.g. Raspberry Pi) fixing their backward compatibility gpiochip renumbering
187            // issues using symlinks that then conflict with dynamically created gpiochips.
188            // This can result in the gpio-sim users inadvertently driving ACTUAL hardware,
189            // which is obviously something that can not be allowed, so bail out.
190            let metadata = fs::symlink_metadata(&c.dev_path)?;
191            assert!(
192                !metadata.is_symlink(),
193                "A symlink ({}) is masking device {}.  Please remove the symlink.",
194                &c.dev_path.display(),
195                &chip_name,
196            );
197            let mut sysfs_path = PathBuf::from("/sys/devices/platform");
198            sysfs_path.push(&dev_name);
199            sysfs_path.push(&chip_name);
200            c.sysfs_path = sysfs_path;
201            c.chip_name = chip_name;
202            c.dev_name.clone_from(&dev_name);
203        }
204        Ok(())
205    }
206}
207
208impl Drop for Sim {
209    fn drop(&mut self) {
210        self.cleanup_configfs();
211    }
212}
213
214/// A live simulated chip.
215#[derive(Debug)]
216pub struct Chip {
217    /// The path to the chip in /dev
218    ///
219    /// e.g. `/dev/gpiopchip0`
220    dev_path: PathBuf,
221
222    /// The name of the gpiochip in /dev and sysfs.
223    ///
224    /// e.g. `gpiochip0`
225    pub chip_name: String,
226
227    /// The name of the device in sysfs.
228    ///
229    /// e.g. `gpio-sim.0`
230    pub dev_name: String,
231
232    /// The path to the chip in /sys/device/platform.
233    ///
234    /// e.g. `/sys/devices/platform/gpio-sim.0`
235    sysfs_path: PathBuf,
236
237    /// The configuration for the chip.
238    cfg: Bank,
239}
240
241impl Chip {
242    /// The configuration for the chip
243    pub fn config(&self) -> &Bank {
244        &self.cfg
245    }
246
247    /// The path to the chip in /dev
248    ///
249    /// e.g. `/dev/gpiopchip0`
250    pub fn dev_path(&self) -> &PathBuf {
251        &self.dev_path
252    }
253
254    /// Pull a line to simulate the line being externally driven.
255    pub fn set_pull(&self, offset: Offset, pull: Level) -> Result<()> {
256        let value = match pull {
257            Level::Low => "pull-down",
258            Level::High => "pull-up",
259        };
260        let path = format!("sim_gpio{offset}/pull");
261        fs::write(self.sysfs_path.join(path), value).map_err(Error::IoError)
262    }
263
264    /// Pull a line up to simulate the line being externally driven high.
265    pub fn pullup(&self, offset: Offset) -> Result<()> {
266        self.set_pull(offset, Level::High)
267    }
268
269    /// Pull a line down to simulate the line being externally driven low.
270    pub fn pulldown(&self, offset: Offset) -> Result<()> {
271        self.set_pull(offset, Level::Low)
272    }
273
274    /// Toggle the pull on a line.
275    pub fn toggle(&self, offset: Offset) -> Result<Level> {
276        let value = match self.get_pull(offset)? {
277            Level::High => Level::Low,
278            Level::Low => Level::High,
279        };
280        self.set_pull(offset, value)?;
281        Ok(value)
282    }
283
284    fn get_attr(&self, offset: Offset, attr: &str) -> Result<String> {
285        let path = format!("sim_gpio{offset}/{attr}");
286        fs::read_to_string(self.sysfs_path.join(path))
287            .map(|s| s.trim().to_string())
288            .map_err(Error::IoError)
289    }
290
291    /// Get the current state of the simulated external pull on a line.
292    pub fn get_pull(&self, offset: Offset) -> Result<Level> {
293        let pull = self.get_attr(offset, "pull")?;
294        match pull.as_str() {
295            "pull-down" => Ok(Level::Low),
296            "pull-up" => Ok(Level::High),
297            _ => Err(Error::UnexpectedValue(pull)),
298        }
299    }
300
301    /// Get the current output value for a simulated output line.
302    pub fn get_level(&self, offset: Offset) -> Result<Level> {
303        let val = self.get_attr(offset, "value")?;
304        match val.as_str() {
305            "0" => Ok(Level::Low),
306            "1" => Ok(Level::High),
307            _ => Err(Error::UnexpectedValue(val)),
308        }
309    }
310}
311impl PartialEq for Chip {
312    fn eq(&self, other: &Self) -> bool {
313        self.dev_path == other.dev_path
314    }
315}
316impl Eq for Chip {}
317
318/// Start building a GPIO simulator.
319pub fn builder() -> Builder {
320    Builder::default()
321}
322
323/// A basic single bank/chip sim.
324///
325/// This is sufficient for tests that do not require named lines, hogged lines
326/// or multiple chips.
327pub struct Simpleton {
328    pub sim: Sim,
329}
330
331impl Simpleton {
332    /// Build a basic single bank sim and take it live.
333    ///
334    /// This is sufficient for tests that do not require named lines, hogged lines
335    /// or multiple chips.
336    ///
337    ///
338    pub fn new(num_lines: u32) -> Simpleton {
339        Simpleton {
340            sim: builder()
341                .with_bank(&Bank::new(num_lines, "simpleton"))
342                .live()
343                .unwrap(),
344        }
345    }
346
347    /// Return the only chip simulated by the Simpleton.
348    pub fn chip(&self) -> &Chip {
349        &self.sim.chips[0]
350    }
351
352    /// The configuration for the Simpleton chip
353    pub fn config(&self) -> &Bank {
354        &self.sim.chips[0].cfg
355    }
356
357    /// Return path to the chip in /dev.
358    pub fn dev_path(&self) -> &PathBuf {
359        &self.sim.chips[0].dev_path
360    }
361
362    /// Pull a line to simulate the line being externally driven.
363    pub fn set_pull(&self, offset: Offset, pull: Level) -> Result<()> {
364        self.sim.chips[0].set_pull(offset, pull)
365    }
366
367    /// Pull a line up to simulate the line being externally driven high.
368    pub fn pullup(&self, offset: Offset) -> Result<()> {
369        self.set_pull(offset, Level::High)
370    }
371
372    /// Pull a line down to simulate the line being externally driven low.
373    pub fn pulldown(&self, offset: Offset) -> Result<()> {
374        self.set_pull(offset, Level::Low)
375    }
376
377    /// Toggle the pull on a line.
378    pub fn toggle(&self, offset: Offset) -> Result<Level> {
379        self.sim.chips[0].toggle(offset)
380    }
381
382    /// Get the current state of the simulated external pull on a line.
383    pub fn get_pull(&self, offset: Offset) -> Result<Level> {
384        self.sim.chips[0].get_pull(offset)
385    }
386
387    /// Get the current output value for a simulated output line.
388    pub fn get_level(&self, offset: Offset) -> Result<Level> {
389        self.sim.chips[0].get_level(offset)
390    }
391}
392
393/// A builder of simulators.
394///
395/// Collects the configuration for the simulator, and then creates
396/// the simulator when taken live.
397#[derive(Clone, Debug, Default, Eq, PartialEq)]
398pub struct Builder {
399    /// The name for the simulator in the configfs space.
400    ///
401    /// If None when [`live`] is called then a unique name is generated.
402    ///
403    /// [`live`]: Builder::live
404    pub name: Option<String>,
405
406    /// The details of the banks to be simulated.
407    ///
408    /// Each bank becomes a chip when the simulator goes live.
409    pub banks: Vec<Bank>,
410}
411
412impl Builder {
413    /// A convenience function to add a bank to the configuration.
414    pub fn with_bank(&mut self, bank: &Bank) -> &mut Self {
415        self.banks.push(bank.clone());
416        self
417    }
418
419    /// A convenience function to specify the name for the simulator.
420    ///
421    /// The name must be unique or going live will fail.
422    pub fn with_name<N: Into<String>>(&mut self, name: N) -> &mut Self {
423        self.name = Some(name.into());
424        self
425    }
426
427    /// Take the builder config live and return the created simulator.
428    ///
429    /// If no name has been provided for the builder then one is generated
430    /// in the format `<app>-p<pid>-<N>` where:
431    ///  - the app name is drawn from `argv[0]` of the executable
432    ///  - pid is the process id
433    ///  - N is a counter of sims taken live by this process, starting at 0
434    pub fn live(&mut self) -> Result<Sim> {
435        let name = match &self.name {
436            Some(n) => n.clone(),
437            None => default_name(),
438        };
439        let sim_dir = find_configfs()?.join(&name);
440        if sim_dir.exists() {
441            return Err(Error::SimulatorExists(name));
442        }
443        fs::create_dir(&sim_dir)?;
444
445        let mut sim = Sim {
446            name,
447            chips: Vec::new(),
448            dir: sim_dir,
449        };
450        for b in &self.banks {
451            sim.chips.push(Chip {
452                cfg: b.clone(),
453                dev_path: PathBuf::default(),
454                chip_name: String::default(),
455                dev_name: String::default(),
456                sysfs_path: PathBuf::default(),
457            })
458        }
459        sim.live()?;
460
461        Ok(sim)
462    }
463}
464
465/// The offset of a line on a chip.
466pub type Offset = u32;
467
468/// A map from offset to T.
469pub type OffsetMap<T> = HashMap<Offset, T, BuildHasherDefault<OffsetHasher>>;
470
471/// A simple identity hasher for maps using Offsets as keys.
472#[derive(Default)]
473pub struct OffsetHasher(u64);
474
475impl Hasher for OffsetHasher {
476    fn finish(&self) -> u64 {
477        self.0
478    }
479
480    fn write(&mut self, _: &[u8]) {
481        panic!("OffsetHasher key must be u32")
482    }
483
484    fn write_u32(&mut self, n: u32) {
485        self.0 = n.into()
486    }
487}
488
489/// The configuration for a single simulated chip.
490#[derive(Clone, Debug, Default, Eq, PartialEq)]
491pub struct Bank {
492    /// The number of lines simulated by this bank.
493    pub num_lines: u32,
494
495    /// The label of the chip.
496    pub label: String,
497
498    /// Lines assigned a name.
499    pub names: OffsetMap<String>,
500
501    /// Lines that appear to be already in use by some other entity.
502    pub hogs: OffsetMap<Hog>,
503}
504
505impl Bank {
506    /// Basic constructor.
507    pub fn new<N: Into<String>>(num_lines: u32, label: N) -> Bank {
508        Bank {
509            num_lines,
510            label: label.into(),
511            names: OffsetMap::default(),
512            hogs: OffsetMap::default(),
513        }
514    }
515
516    /// Assign a name to a line on the chip.
517    pub fn name<N: Into<String>>(&mut self, offset: Offset, name: N) -> &mut Self {
518        self.names.insert(offset, name.into());
519        self
520    }
521
522    /// Remove the name from a line.
523    pub fn unname(&mut self, offset: Offset) -> &mut Self {
524        self.names.remove(&offset);
525        self
526    }
527
528    /// Add a hog on a line on the chip.
529    ///
530    /// A "hog" simulates some other user holding the line.
531    ///
532    /// If a line is hogged it is not available to be requested via the uAPI.
533    pub fn hog<N: Into<String>>(
534        &mut self,
535        offset: Offset,
536        consumer: N,
537        direction: Direction,
538    ) -> &mut Self {
539        self.hogs.insert(
540            offset,
541            Hog {
542                direction,
543                consumer: consumer.into(),
544            },
545        );
546        self
547    }
548
549    /// Unhog a line on the chip.
550    pub fn unhog(&mut self, offset: Offset) -> &mut Self {
551        self.hogs.remove(&offset);
552        self
553    }
554}
555
556/// The configuration for a hogged line.
557///
558/// A "hogged" line appears to be already requested by a consumer.
559#[derive(Clone, Debug, Eq, PartialEq)]
560pub struct Hog {
561    /// The name of the consumer that appears to be using the line.
562    pub consumer: String,
563
564    /// The requested direction for the hogged line, and if an
565    /// output then the pull.
566    pub direction: Direction,
567}
568
569/// The direction, and for outputs the pulled value, of a hogged line.
570#[derive(Clone, Copy, Debug, Eq, PartialEq)]
571pub enum Direction {
572    /// Hogged line is requested as an input.
573    Input,
574
575    /// Hogged line is requested as an output pulled low.
576    OutputLow,
577
578    /// Hogged line is requested as an output pulled high.
579    OutputHigh,
580}
581
582impl Direction {
583    fn as_str(&self) -> &'static str {
584        match self {
585            Direction::Input => "input",
586            Direction::OutputHigh => "output-high",
587            Direction::OutputLow => "output-low",
588        }
589    }
590}
591
592/// The physical value of a line.
593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
594pub enum Level {
595    /// The line is  physically high.
596    High,
597
598    /// The line is physically low.
599    Low,
600}
601
602impl Level {
603    /// Toggle the level between high and low.
604    pub fn toggle(&self) -> Level {
605        match self {
606            Level::High => Level::Low,
607            Level::Low => Level::High,
608        }
609    }
610}
611
612/// Create a unique, but predictable, name for the simulator.
613///
614/// The name format is `<app>-p<pid>-<N>[-<instance>]`
615/// where:
616///   - the app name provided by the caller
617///   - pid is the process id
618///   - N is a counter of names generated, starting at 0
619///   - instance is optionally provided by the caller
620pub fn unique_name(app: &str, instance: Option<&str>) -> String {
621    static NAME_COUNT: AtomicU32 = AtomicU32::new(0);
622
623    let mut name = format!(
624        "{}-p{}-{}",
625        app,
626        process::id(),
627        NAME_COUNT.fetch_add(1, Ordering::Relaxed)
628    );
629    if let Some(i) = instance {
630        name += "-";
631        name += i;
632    }
633    name
634}
635
636// Helper to write to simulator configuration files.
637fn write_attr<D: AsRef<[u8]>>(p: &Path, file: &str, data: D) -> Result<()> {
638    let path = p.join(file);
639    fs::write(path, data).map_err(Error::IoError)
640}
641
642// Helper to read from simulator attribute files.
643fn read_attr(p: &Path, file: &str) -> Result<String> {
644    let path = p.join(file);
645    fs::read_to_string(path)
646        .map(|s| s.trim().to_string())
647        .map_err(Error::IoError)
648}
649
650fn app_name() -> String {
651    if let Some(app) = env::args_os().next() {
652        if let Some(path) = Path::new(app.as_os_str()).file_name() {
653            if let Some(app) = path.to_str() {
654                return app.into();
655            }
656        }
657    }
658    "gpiosim".into()
659}
660
661fn default_name() -> String {
662    unique_name(&app_name(), None)
663}
664
665fn configfs_mountpoint() -> Option<PathBuf> {
666    if let Ok(f) = File::open("/proc/mounts") {
667        let r = BufReader::new(f);
668        for line in r.lines().map_while(|x| x.ok()) {
669            let words: Vec<&str> = line.split_ascii_whitespace().collect();
670            if words.len() >= 6 && words[2] == "configfs" {
671                return Some(PathBuf::from(words[1]));
672            }
673        }
674    }
675    None
676}
677
678// check if configfs is mounted, and if so where.
679fn find_configfs() -> Result<PathBuf> {
680    // Assume default location for starters
681    let path: PathBuf = "/sys/kernel/config/gpio-sim".into();
682    if path.exists() {
683        return Ok(path);
684    }
685    // Perhaps gpio-sim module is not loaded - so load it
686    let output = process::Command::new("modprobe")
687        .arg("gpio-sim")
688        .output()
689        .map_err(|e| Error::CommandError("modprobe".into(), Box::new(e)))?;
690    if !output.status.success() {
691        return Err(Error::ModuleLoadError(OsString::from_vec(output.stderr)));
692    }
693    for _ in 0..10 {
694        if path.exists() {
695            return Ok(path);
696        }
697        // Loading gpio-sim should mount configfs, but maybe it isn't in the
698        // standard location, so check mounts...
699        if let Some(mut cfgfs) = configfs_mountpoint() {
700            cfgfs.push("gpio-sim");
701            if path.exists() {
702                return Ok(cfgfs);
703            }
704        }
705        sleep(Duration::from_millis(100));
706    }
707    Err(Error::ConfigfsNotFound)
708}
709
710/// The result for [`gpiosim`] functions.
711///
712/// [`gpiosim`]: crate
713pub type Result<T> = std::result::Result<T, Error>;
714
715/// Errors returned by [`gpiosim`] functions.
716///
717/// [`gpiosim`]: crate
718#[derive(Debug, thiserror::Error)]
719pub enum Error {
720    /// Could not find the configfs mountpoint.
721    #[error("Could not find configsfs")]
722    ConfigfsNotFound,
723
724    /// An error detected while loading the gpio-sim kernel module.
725    #[error("Could not load gpio-sim module: {0:?}")]
726    ModuleLoadError(OsString),
727
728    /// Attempt to take a simulator live with a name of an active simulator.
729    #[error("Simulator with name {0:?} already exists")]
730    SimulatorExists(String),
731
732    /// An unexpected value was read from a configfs or sysfs attribute file.
733    #[error("Read unexpected attr value {0:?}")]
734    UnexpectedValue(String),
735
736    /// An IO error detected while accessing a configfs or sysfs attribute file
737    #[error(transparent)]
738    IoError(#[from] std::io::Error),
739
740    /// An error detected while executing an external command.
741    #[error("Command {0} returned error {1}")]
742    CommandError(String, Box<dyn std::error::Error>),
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use Direction::*;
749
750    #[test]
751    fn unique_name_default() {
752        let name = unique_name("my_app", None);
753        assert!(name.starts_with("my_app-p"));
754    }
755
756    #[test]
757    fn unique_name_explicit() {
758        let name = unique_name("my_app", Some("test2"));
759        assert!(name.starts_with("my_app-p"));
760        assert!(name.ends_with("-test2"));
761    }
762
763    #[test]
764    fn bank_constructor_default() {
765        let c = Bank::default();
766        assert_eq!(c.num_lines, 0);
767        assert!(c.label.is_empty());
768        assert_eq!(c.names.len(), 0);
769        assert_eq!(c.hogs.len(), 0);
770    }
771
772    #[test]
773    fn bank_name() {
774        let mut c = Bank::default();
775        c.name(3, "pinata");
776        assert_eq!(c.names.len(), 1);
777        assert_eq!(c.names[&3], "pinata");
778        c.name(3, "pineapple");
779        assert_eq!(c.names.len(), 1);
780        assert_eq!(c.names[&3], "pineapple");
781        c.name(0, "nada");
782        assert_eq!(c.names.len(), 2);
783        assert_eq!(c.names[&0], "nada");
784    }
785
786    #[test]
787    fn bank_unname() {
788        let mut c = Bank::default();
789        c.name(3, "pinata");
790        c.name(0, "nada");
791        assert_eq!(c.names.len(), 2);
792        c.unname(3);
793        assert!(!c.names.contains_key(&3));
794        assert_eq!(c.names.len(), 1);
795        assert_eq!(c.names[&0], "nada");
796    }
797
798    #[test]
799    fn bank_hog() {
800        let mut c = Bank::default();
801        c.hog(3, "pinata", Direction::Input);
802        assert_eq!(c.hogs.len(), 1);
803        c.hog(2, "piggly", Direction::OutputLow);
804        assert_eq!(c.hogs.len(), 2);
805        c.hog(1, "wiggly", Direction::OutputHigh);
806        assert_eq!(c.hogs.len(), 3);
807        assert_eq!(c.hogs[&3].consumer, "pinata");
808        assert_eq!(c.hogs[&2].consumer, "piggly");
809        assert_eq!(c.hogs[&1].consumer, "wiggly");
810        assert_eq!(c.hogs[&3].direction, Direction::Input);
811        assert_eq!(c.hogs[&2].direction, Direction::OutputLow);
812        assert_eq!(c.hogs[&1].direction, Direction::OutputHigh);
813        // overwrite
814        c.hog(2, "wiggly", Direction::OutputHigh);
815        assert_eq!(c.hogs[&2].consumer, "wiggly");
816        assert_eq!(c.hogs[&2].direction, Direction::OutputHigh);
817        assert_eq!(c.hogs.len(), 3);
818    }
819
820    #[test]
821    fn bank_unhog() {
822        let mut c = Bank::default();
823        c.hog(3, "pinata", Direction::Input);
824        c.hog(2, "piggly", Direction::OutputLow);
825        c.hog(1, "wiggly", Direction::OutputHigh);
826        assert_eq!(c.hogs.len(), 3);
827        c.unhog(2);
828        assert_eq!(c.hogs.len(), 2);
829        assert!(!c.hogs.contains_key(&2));
830        assert_eq!(c.hogs[&3].consumer, "pinata");
831        assert_eq!(c.hogs[&1].consumer, "wiggly");
832        assert_eq!(c.hogs[&3].direction, Direction::Input);
833        assert_eq!(c.hogs[&1].direction, Direction::OutputHigh);
834    }
835
836    #[test]
837    fn builder_with_bank() {
838        let mut builder = builder();
839        builder
840            .with_bank(
841                Bank::new(8, "fish")
842                    .name(3, "banana")
843                    .name(5, "apple")
844                    .hog(5, "breath", Input),
845            )
846            .with_bank(
847                Bank::new(42, "babel")
848                    .name(3, "piƱata")
849                    .hog(2, "hogster", OutputHigh)
850                    .hog(7, "hogster", OutputLow),
851            );
852        assert_eq!(builder.banks.len(), 2);
853        assert_eq!(builder.banks[0].num_lines, 8);
854        assert_eq!(builder.banks[0].names.len(), 2);
855        assert_eq!(builder.banks[0].hogs.len(), 1);
856        assert_eq!(builder.banks[1].num_lines, 42);
857        assert_eq!(builder.banks[1].names.len(), 1);
858        assert_eq!(builder.banks[1].hogs.len(), 2);
859    }
860
861    #[test]
862    fn builder_with_name() {
863        let mut builder = builder();
864        assert!(builder.name.is_none());
865        builder.with_name("banana");
866        assert!(builder.name.is_some());
867        assert_eq!(builder.name.unwrap(), "banana");
868    }
869}