diff --git a/Cargo.lock b/Cargo.lock index 92d8e22..88dd8bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,11 +67,50 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "chiptool" version = "0.1.0" dependencies = [ "anyhow", + "assert_cmd", "clap", "env_logger", "inflections", @@ -80,8 +119,11 @@ dependencies = [ "quote", "regex", "serde", + "serde_json", "serde_yaml", "svd-parser", + "tempfile", + "toml", ] [[package]] @@ -130,6 +172,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "env_filter" version = "0.1.4" @@ -159,6 +207,50 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -171,6 +263,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.12.1" @@ -178,7 +276,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -223,6 +323,24 @@ dependencies = [ "syn", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.28" @@ -262,6 +380,43 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -280,6 +435,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "regex" version = "1.12.2" @@ -315,12 +476,31 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -351,6 +531,28 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_yaml" version = "0.9.34-deprecated" @@ -402,6 +604,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -422,12 +643,57 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -440,6 +706,67 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -454,3 +781,103 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 66f2a4a..b528c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,11 @@ regex = "1.10.3" serde = { version = "1.0.196", features = [ "derive" ]} svd-parser = { git = "https://github.com/embassy-rs/svd.git", rev = "f4f192d083946fc6227db7ac17eba8aa58bb9d4c", features = ["derive-from", "expand"] } #svd-parser = { path = "./svd/svd-parser", features = ["derive-from", "expand"] } +serde_json = "1.0" # Development has stopped for `serde_yaml` serde_yaml = "=0.9.34-deprecated" +toml = "1.0" + +[dev-dependencies] +assert_cmd = "2.1" +tempfile = "3.26" diff --git a/src/main.rs b/src/main.rs index 0448052..7d35c69 100755 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,16 @@ use anyhow::{bail, Context, Result}; use chiptool::{generate, svd2ir}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use log::*; use regex::Regex; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::collections::BTreeSet; use std::fs; -use std::io::Read; -use std::path::PathBuf; +use std::io; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; use std::{fs::File, io::stdout}; use svd_parser::ValidateLevel; @@ -32,43 +35,78 @@ enum Subcommand { GenBlock(GenBlock), } -/// Extract peripheral from SVD to YAML +/// Extract peripheral from SVD to Yaml/Toml/Json IR in stdout #[derive(Parser)] struct ExtractPeripheral { /// SVD file path #[clap(long)] - svd: String, + #[clap(value_name = "FILE")] + svd: PathBuf, /// Peripheral from the SVD #[clap(long)] + #[clap(value_name = "NAME")] peripheral: String, - /// Transforms file path + /// Peripheral IR format + #[clap(value_name = "FORMAT")] + #[clap(long, default_value = "yaml")] + peripheral_format: Format, + /// Transform file paths #[clap(long)] - transform: Vec, + #[clap(value_name = "FILES")] + transform: Vec, + /// Transform IR format + #[clap(value_name = "FORMAT")] + #[clap(long, default_value = "auto")] + transform_format: AutoFormat, } -/// Extract all peripherals from SVD to YAML +/// Extract all peripherals from SVD to Yaml/Toml/Json IR files #[derive(Parser)] struct ExtractAll { /// SVD file path #[clap(long)] - svd: String, - /// Output directory. Each peripheral will be created as a YAML file here. + #[clap(value_name = "FILE")] + svd: PathBuf, + /// Output directory. Each peripheral will be created as a Yaml/Toml/Json IR file here. #[clap(short, long)] - output: String, + #[clap(value_name = "DIRECTORY")] + output: PathBuf, + /// Output IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "yaml")] + output_format: Format, } -/// Apply transform to YAML +/// Apply transform to Yaml/Toml/Json IR #[derive(Parser)] struct Transform { - /// Input YAML path + /// Input IR file path #[clap(short, long)] - input: String, - /// Output YAML path + #[clap(value_name = "FILE")] + input: PathBuf, + /// Input IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "auto")] + input_format: AutoFormat, + /// Output IR file path #[clap(short, long)] - output: String, - /// Transforms file path + #[clap(value_name = "FILE")] + output: PathBuf, + /// Output IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "auto")] + output_format: AutoFormat, + /// Transform file path #[clap(short, long)] - transform: String, + #[clap(value_name = "FILE")] + transform: PathBuf, + /// Transform IR format + #[clap(value_name = "FORMAT")] + #[clap(long, default_value = "auto")] + transform_format: AutoFormat, } /// Generate a PAC directly from a SVD @@ -76,19 +114,32 @@ struct Transform { struct Generate { /// SVD file path #[clap(long)] - svd: String, - /// Transforms file path + #[clap(value_name = "FILE")] + svd: PathBuf, + /// Transform file paths #[clap(long)] - transform: Vec, + #[clap(value_name = "FILES")] + transform: Vec, + /// Transform IR format + #[clap(value_name = "FORMAT")] + #[clap(long, default_value = "auto")] + transform_format: AutoFormat, #[clap(flatten)] gen_shared: GenShared, } -/// Reformat a YAML +/// Reformat Yaml/Toml/Json IR files #[derive(Parser)] struct Fmt { - /// Peripheral file path - files: Vec, + /// Peripheral IR file paths + #[arg(required = true)] + files: Vec, + + /// Peripheral IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "auto")] + file_format: AutoFormat, /// Error if incorrectly formatted, instead of fixing. #[clap(long)] check: bool, @@ -97,12 +148,18 @@ struct Fmt { remove_unused: bool, } -/// Check a YAML for errors. +/// Check Yaml/Toml/Json IR files for errors #[derive(Parser)] struct Check { - /// Peripheral file path - files: Vec, + /// Peripheral IR file paths + #[arg(required = true)] + files: Vec, + /// Peripheral IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "auto")] + file_format: AutoFormat, #[clap(long)] allow_register_overlap: bool, #[clap(long)] @@ -115,15 +172,22 @@ struct Check { allow_unused_fieldsets: bool, } -/// Generate Rust code from a YAML register block +/// Generate Rust code from a Yaml/Toml/Json IR register block #[derive(Parser)] struct GenBlock { - /// Input YAML path + /// Input IR file path #[clap(short, long)] - input: String, - /// Output Rust code path + #[clap(value_name = "FILE")] + input: PathBuf, + /// Input IR format + #[clap(long)] + #[clap(value_name = "FORMAT")] + #[clap(default_value = "auto")] + input_format: AutoFormat, + /// Output Rust file path #[clap(short, long)] - output: String, + #[clap(value_name = "FILE")] + output: PathBuf, #[clap(flatten)] gen_shared: GenShared, } @@ -150,6 +214,84 @@ struct GenShared { yes_defmt: bool, } +#[derive(Copy, Clone, ValueEnum)] +enum AutoFormat { + Auto, + Yaml, + Toml, + Json, +} +impl AutoFormat { + pub fn get_format(&self, path: &Path) -> Result { + let format = match self { + Self::Auto => { + let some_file_name = path.file_name().and_then(|x| x.to_str()); + match some_file_name { + Some(x) if x.ends_with(Format::Yaml.ext()) => Format::Yaml, + Some(x) if x.ends_with(Format::Toml.ext()) => Format::Toml, + Some(x) if x.ends_with(Format::Json.ext()) => Format::Json, + _ => bail!("Cannot determine the format from the path {path:?}"), + } + } + Self::Yaml => Format::Yaml, + Self::Toml => Format::Toml, + Self::Json => Format::Json, + }; + Ok(format) + } +} + +#[derive(ValueEnum, Copy, Clone)] +enum Format { + Yaml, + Toml, + Json, +} +impl Format { + pub fn ext(&self) -> &'static str { + match self { + Self::Yaml => ".yaml", + Self::Toml => ".toml", + Self::Json => ".json", + } + } + pub fn load_from_path(&self, path: &Path) -> Result { + let f = File::open(path).with_context(|| format!("Cannot open the file {path:?}"))?; + self.load_from_reader(f) + } + pub fn load_from_reader(&self, reader: R) -> Result { + match self { + Self::Yaml => serde_yaml::from_reader(reader).context("Cannot deserialize the Yaml"), + Self::Toml => { + let s = io::read_to_string(reader).context("Cannot read the TOml")?; + toml::from_str(&s).context("Cannot deserialize the Toml") + } + Self::Json => serde_json::from_reader(reader).context("Cannot deserialize the Json"), + } + } + pub fn save_to_path(&self, path: &Path, value: &T) -> Result<()> { + let f = File::create(path).with_context(|| format!("Cannot create the file {path:?}"))?; + self.save_to_writer(f, value) + } + pub fn save_to_writer(&self, mut writer: W, value: &T) -> Result<()> { + match self { + Self::Yaml => { + serde_yaml::to_writer(writer, value).context("Cannot serialize the Yaml")? + } + Self::Toml => { + let data = toml::to_string(value).context("Cannot serialize the Toml")?; + writer + .write_all(data.as_bytes()) + .context("Cannot write the Toml")?; + } + Self::Json => { + serde_json::to_writer_pretty(writer, value).context("Cannot serialize the Json")? + } + } + Ok(()) + } +} + fn main() -> Result<()> { env_logger::init(); @@ -166,7 +308,7 @@ fn main() -> Result<()> { } } -fn load_svd(path: &str) -> Result { +fn load_svd(path: &Path) -> Result { let xml = &mut String::new(); File::open(path) .context("Cannot open the SVD file")? @@ -180,9 +322,11 @@ fn load_svd(path: &str) -> Result { Ok(device) } -fn load_config(path: &str) -> Result { - let config = fs::read(path).context("Cannot read the config file")?; - serde_yaml::from_slice(&config).context("cannot deserialize config") +fn load_config(path: &Path, config_format: AutoFormat) -> Result { + config_format + .get_format(path) + .and_then(|format| format.load_from_path(path)) + .with_context(|| format!("Cannot read the config file {path:?}")) } fn extract_peripheral(args: ExtractPeripheral) -> Result<()> { @@ -233,13 +377,13 @@ fn extract_peripheral(args: ExtractPeripheral) -> Result<()> { } for transform in args.transform { - apply_transform(&mut ir, transform)?; + apply_transform(&mut ir, &transform, args.transform_format)?; } // Ensure consistent sort order in the YAML. chiptool::transform::sort::Sort {}.run(&mut ir).unwrap(); - serde_yaml::to_writer(stdout(), &ir).unwrap(); + args.peripheral_format.save_to_writer(stdout(), &ir)?; Ok(()) } @@ -263,8 +407,9 @@ fn extract_all(args: ExtractAll) -> Result<()> { // Ensure consistent sort order in the YAML. chiptool::transform::sort::Sort {}.run(&mut ir).unwrap(); - let f = File::create(PathBuf::from(&args.output).join(format!("{}.yaml", p.name)))?; - serde_yaml::to_writer(f, &ir).unwrap(); + let filename = format!("{}{}", p.name, args.output_format.ext()); + let path = args.output.join(filename); + args.output_format.save_to_path(&path, &ir)?; } Ok(()) @@ -279,7 +424,7 @@ fn gen(args: Generate) -> Result<()> { chiptool::transform::map_descriptions(&mut ir, |d| re.replace_all(d, " ").into_owned())?; for transform in args.transform { - apply_transform(&mut ir, transform)?; + apply_transform(&mut ir, &transform, args.transform_format)?; } let generate_opts = get_generate_opts(args.gen_shared)?; @@ -293,20 +438,31 @@ fn gen(args: Generate) -> Result<()> { } fn transform(args: Transform) -> Result<()> { - let data = fs::read(&args.input)?; - let mut ir: IR = serde_yaml::from_slice(&data)?; - apply_transform(&mut ir, args.transform)?; + let mut ir: IR = args + .input_format + .get_format(&args.input) + .and_then(|format| format.load_from_path(&args.input)) + .with_context(|| format!("Cannot load the input IR file {:?}", args.input))?; + + apply_transform(&mut ir, &args.transform, args.transform_format)?; - let data = serde_yaml::to_string(&ir)?; - fs::write(&args.output, data.as_bytes())?; + args.output_format + .get_format(&args.output) + .and_then(|format| format.save_to_path(&args.output, &ir)) + .with_context(|| format!("Cannot save the output IR file {:?}", args.output))?; Ok(()) } fn fmt(args: Fmt) -> Result<()> { for file in args.files { - let got_data = fs::read(&file)?; - let mut ir: IR = serde_yaml::from_slice(&got_data)?; + let got_data = + fs::read(&file).with_context(|| format!("Cannot read the IR file {file:?}"))?; + let mut ir: IR = args + .file_format + .get_format(&file) + .and_then(|format| format.load_from_reader(got_data.as_slice())) + .with_context(|| format!("Cannot load the IR file {file:?}"))?; if args.remove_unused { let mut used_enums = BTreeSet::new(); @@ -351,11 +507,15 @@ fn fmt(args: Fmt) -> Result<()> { } } - let want_data = serde_yaml::to_string(&ir)?; + let mut want_data = Vec::new(); + args.file_format + .get_format(&file) + .and_then(|format| format.save_to_writer(&mut want_data, &ir)) + .with_context(|| format!("Cannot serialize the IR of file {file:?}"))?; - if got_data != want_data.as_bytes() { + if got_data != want_data.as_slice() { if args.check { - bail!("File {} is not correctly formatted", &file); + bail!("File {:?} is not correctly formatted", &file); } else { fs::write(&file, want_data)?; } @@ -376,12 +536,15 @@ fn check(args: Check) -> Result<()> { let mut fails = 0; for file in args.files { - let got_data = fs::read(&file)?; - let ir: IR = serde_yaml::from_slice(&got_data)?; + let ir: IR = args + .file_format + .get_format(&file) + .and_then(|format| format.load_from_path(&file)) + .with_context(|| format!("Cannot load the IR file {file:?}"))?; let errs = chiptool::validate::validate(&ir, opts.clone()); fails += errs.len(); for e in errs { - println!("{}: {}", &file, e); + println!("{:?}: {}", &file, e); } } @@ -393,8 +556,11 @@ fn check(args: Check) -> Result<()> { } fn gen_block(args: GenBlock) -> Result<()> { - let data = fs::read(&args.input)?; - let mut ir: IR = serde_yaml::from_slice(&data)?; + let mut ir: IR = args + .input_format + .get_format(&args.input) + .and_then(|format| format.load_from_path(&args.input)) + .with_context(|| format!("Cannot load the input IR file {:?}", args.input))?; chiptool::transform::sanitize::Sanitize {} .run(&mut ir) @@ -418,13 +584,13 @@ struct Config { transforms: Vec, } -fn apply_transform>(ir: &mut IR, p: P) -> anyhow::Result<()> { - info!("applying transform {}", p.as_ref().display()); - let config = load_config(p.as_ref().to_str().unwrap())?; +fn apply_transform(ir: &mut IR, p: &Path, transform_format: AutoFormat) -> Result<()> { + info!("applying transform {:?}", p); + let config = load_config(p, transform_format)?; for include in &config.includes { - let subp = p.as_ref().parent().unwrap().join(include); - apply_transform(ir, subp)?; + let subp = p.join(include); + apply_transform(ir, &subp, transform_format)?; } for transform in &config.transforms { info!("running {:?}", transform); diff --git a/tests/ARM_Example.svd b/tests/ARM_Example.svd new file mode 100644 index 0000000..0da2752 --- /dev/null +++ b/tests/ARM_Example.svd @@ -0,0 +1,789 @@ + + + + + + + + + ARM Ltd. + ARM + ARM_Example + ARMCM3 + 1.2 + ARM 32-bit Cortex-M3 Microcontroller based device, CPU clock up to 80MHz, etc. + + ARM Limited (ARM) is supplying this software for use with Cortex-M\n + processor based microcontroller, but can be equally used for other\n + suitable processor architectures. This file can be freely distributed.\n + Modifications to this file shall be clearly marked.\n + \n + THIS SOFTWARE IS PROVIDED "AS IS". NO WARRANTIES, WHETHER EXPRESS, IMPLIED\n + OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF\n + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE APPLY TO THIS SOFTWARE.\n + ARM SHALL NOT, IN ANY CIRCUMSTANCES, BE LIABLE FOR SPECIAL, INCIDENTAL, OR\n + CONSEQUENTIAL DAMAGES, FOR ANY REASON WHATSOEVER. + + + CM3 + r1p0 + little + true + false + 3 + false + + 8 + 32 + + 32 + read-write + 0x00000000 + 0xFFFFFFFF + + + + + TIMER0 + 1.0 + 32 Timer / Counter, counting up or down from different sources + TIMER + 0x40010000 + 32 + read-write + + + 0 + 0x100 + registers + + + + TIMER0 + Timer 0 interrupt + 0 + + + + + + CR + Control Register + 0x00 + 32 + read-write + 0x00000000 + 0x1337F7F + + + + + EN + Enable + [0:0] + read-write + + + Disable + Timer is disabled and does not operate + 0 + + + Enable + Timer is enabled and can operate + 1 + + + + + + + RST + Reset Timer + [1:1] + write-only + + + No_Action + Write as ZERO if necessary + 0 + + + Reset_Timer + Reset the Timer + 1 + + + + + + + CNT + Counting direction + [3:2] + read-write + + + Count_UP + Timer Counts UO and wraps, if no STOP condition is set + 0 + + + Count_DOWN + Timer Counts DOWN and wraps, if no STOP condition is set + 1 + + + Toggle + Timer Counts up to MAX, then DOWN to ZERO, if no STOP condition is set + 2 + + + + + + + MODE + Operation Mode + [6:4] + read-write + + + Continous + Timer runs continously + 0 + + + Single_ZERO_MAX + Timer counts to 0x00 or 0xFFFFFFFF (depending on CNT) and stops + 1 + + + Single_MATCH + Timer counts to the Value of MATCH Register and stops + 2 + + + Reload_ZERO_MAX + Timer counts to 0x00 or 0xFFFFFFFF (depending on CNT), loads the RELOAD Value and continues + 3 + + + Reload_MATCH + Timer counts to the Value of MATCH Register, loads the RELOAD Value and continues + 4 + + + + + + + PSC + Use Prescaler + [7:7] + read-write + + + Disabled + Prescaler is not used + 0 + + + Enabled + Prescaler is used as divider + 1 + + + + + + + CNTSRC + Timer / Counter Source Divider + [11:8] + read-write + + + CAP_SRC + Capture Source is used directly + 0 + + + CAP_SRC_div2 + Capture Source is divided by 2 + 1 + + + CAP_SRC_div4 + Capture Source is divided by 4 + 2 + + + CAP_SRC_div8 + Capture Source is divided by 8 + 3 + + + CAP_SRC_div16 + Capture Source is divided by 16 + 4 + + + CAP_SRC_div32 + Capture Source is divided by 32 + 5 + + + CAP_SRC_div64 + Capture Source is divided by 64 + 6 + + + CAP_SRC_div128 + Capture Source is divided by 128 + 7 + + + CAP_SRC_div256 + Capture Source is divided by 256 + 8 + + + + + + + CAPSRC + Timer / Counter Capture Source + [15:12] + read-write + + + CClk + Core Clock + 0 + + + GPIOA_0 + GPIO A, PIN 0 + 1 + + + GPIOA_1 + GPIO A, PIN 1 + 2 + + + GPIOA_2 + GPIO A, PIN 2 + 3 + + + GPIOA_3 + GPIO A, PIN 3 + 4 + + + GPIOA_4 + GPIO A, PIN 4 + 5 + + + GPIOA_5 + GPIO A, PIN 5 + 6 + + + GPIOA_6 + GPIO A, PIN 6 + 7 + + + GPIOA_7 + GPIO A, PIN 7 + 8 + + + GPIOB_0 + GPIO B, PIN 0 + 9 + + + GPIOB_1 + GPIO B, PIN 1 + 10 + + + GPIOB_2 + GPIO B, PIN 2 + 11 + + + GPIOB_3 + GPIO B, PIN 3 + 12 + + + GPIOC_0 + GPIO C, PIN 0 + 13 + + + GPIOC_5 + GPIO C, PIN 1 + 14 + + + GPIOC_6 + GPIO C, PIN 2 + 15 + + + + + + + CAPEDGE + Capture Edge, select which Edge should result in a counter increment or decrement + [17:16] + read-write + + + RISING + Only rising edges result in a counter increment or decrement + 0 + + + FALLING + Only falling edges result in a counter increment or decrement + 1 + + + BOTH + Rising and falling edges result in a counter increment or decrement + 2 + + + + + + + TRGEXT + Triggers an other Peripheral + [21:20] + read-write + + + NONE + No Trigger is emitted + 0 + + + DMA1 + DMA Controller 1 is triggered, dependant on MODE + 1 + + + DMA2 + DMA Controller 2 is triggered, dependant on MODE + 2 + + + UART + UART is triggered, dependant on MODE + 3 + + + + + + + RELOAD + Select RELOAD Register n to reload Timer on condition + [25:24] + read-write + + + RELOAD0 + Selects Reload Register number 0 + 0 + + + RELOAD1 + Selects Reload Register number 1 + 1 + + + RELOAD2 + Selects Reload Register number 2 + 2 + + + RELOAD3 + Selects Reload Register number 3 + 3 + + + + + + + IDR + Selects, if Reload Register number is incremented, decremented or not modified + [27:26] + read-write + + + KEEP + Reload Register number does not change automatically + 0 + + + INCREMENT + Reload Register number is incremented on each match + 1 + + + DECREMENT + Reload Register number is decremented on each match + 2 + + + + + + + S + Starts and Stops the Timer / Counter + [31:31] + read-write + + + STOP + Timer / Counter is stopped + 0 + + + START + Timer / Counter is started + 1 + + + + + + + + + SR + Status Register + 0x04 + 16 + read-write + 0x00000000 + 0xD701 + + + + + RUN + Shows if Timer is running or not + [0:0] + read-only + + + Stopped + Timer is not running + 0 + + + Running + Timer is running + 1 + + + + + + + MATCH + Shows if the MATCH was hit + [8:8] + read-write + + + No_Match + The MATCH condition was not hit + 0 + + + Match_Hit + The MATCH condition was hit + 1 + + + + + + + UN + Shows if an underflow occured. This flag is sticky + [9:9] + read-write + + + No_Underflow + No underflow occured since last clear + 0 + + + Underflow + A minimum of one underflow occured since last clear + 1 + + + + + + + OV + Shows if an overflow occured. This flag is sticky + [10:10] + read-write + + + No_Overflow + No overflow occured since last clear + 0 + + + Overflow_occured + A minimum of one overflow occured since last clear + 1 + + + + + + + RST + Shows if Timer is in RESET state + [12:12] + read-only + + + Ready + Timer is not in RESET state and can operate + 0 + + + In_Reset + Timer is in RESET state and can not operate + 1 + + + + + + + RELOAD + Shows the currently active RELOAD Register + [15:14] + read-only + + + RELOAD0 + Reload Register number 0 is active + 0 + + + RELOAD1 + Reload Register number 1 is active + 1 + + + RELOAD2 + Reload Register number 2 is active + 2 + + + RELOAD3 + Reload Register number 3 is active + 3 + + + + + + + + + INT + Interrupt Register + 0x10 + 16 + read-write + 0x00000000 + 0x0771 + + + + + EN + Interrupt Enable + [0:0] + read-write + + + Disabled + Timer does not generate Interrupts + 0 + + + Enable + Timer triggers the TIMERn Interrupt + 1 + + + + + + + MODE + Interrupt Mode, selects on which condition the Timer should generate an Interrupt + [6:4] + read-write + + + Match + Timer generates an Interrupt when the MATCH condition is hit + 0 + + + Underflow + Timer generates an Interrupt when it underflows + 1 + + + Overflow + Timer generates an Interrupt when it overflows + 2 + + + + + + + + + COUNT + The Counter Register reflects the actual Value of the Timer/Counter + 0x20 + 32 + read-write + 0x00000000 + 0xFFFFFFFF + + + + + MATCH + The Match Register stores the compare Value for the MATCH condition + 0x24 + 32 + read-write + 0x00000000 + 0xFFFFFFFF + + + + + PRESCALE_RD + The Prescale Register stores the Value for the prescaler. The cont event gets divided by this value + 0x28 + 32 + read-only + 0x00000000 + 0xFFFFFFFF + + + + + PRESCALE_WR + The Prescale Register stores the Value for the prescaler. The cont event gets divided by this value + 0x28 + 32 + write-only + 0x00000000 + 0xFFFFFFFF + + + + + + 4 + 4 + RELOAD[%s] + The Reload Register stores the Value the COUNT Register gets reloaded on a when a condition was met. + 0x50 + 32 + read-write + 0x00000000 + 0xFFFFFFFF + + + + + + + TIMER1 + 0x40010100 + + TIMER1 + Timer 1 interrupt + 4 + + + + + + TIMER2 + 0x40010200 + + TIMER2 + Timer 2 interrupt + 6 + + + + \ No newline at end of file diff --git a/tests/basic_commands.rs b/tests/basic_commands.rs new file mode 100644 index 0000000..ed6aa5a --- /dev/null +++ b/tests/basic_commands.rs @@ -0,0 +1,252 @@ +//! Basic command tests. + +use anyhow::{bail, Context, Result}; +use assert_cmd::assert::Assert; +use assert_cmd::cargo::cargo_bin_cmd; +use std::env; +use std::fs; +use std::path::Path; +use tempfile::TempDir; + +trait AssertExt { + /// Print the command output to stdout and stderr. + fn print(self) -> Self; +} +impl AssertExt for Assert { + fn print(self) -> Self { + let output = self.get_output(); + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + self + } +} + +/// Create a temporary dir. +fn create_temporary_dir() -> Result { + TempDir::new().context("Failed to create temporary dir") +} + +/// Copy svd file. +fn copy_svd_to(dir: &Path, filename: &str) -> Result { + let from = "tests/ARM_Example.svd"; + let to = dir.join(filename); + fs::copy(&from, &to).with_context(|| format!("Failed to copy {from:?} to {to:?}")) +} + +/// Copy transform file. +fn copy_transform_to(dir: &Path, filename: &str) -> Result { + let from = "tests/transform.yaml"; + let to = dir.join(filename); + fs::copy(&from, &to).with_context(|| format!("Failed to copy {from:?} to {to:?}")) +} + +/// Check that the files exit in the directory. +fn expect_files_in(dir: &Path, files: &[&str]) -> Result<()> { + let found = fs::read_dir(dir) + .with_context(|| format!("Failed to read dir {dir:?}"))? + .filter_map(|result| { + if let Ok(entry) = result { + if let Ok(file_type) = entry.file_type() { + if file_type.is_file() { + return Some(entry.file_name()); + } + } else { + eprintln!("TEST: bad entry {entry:?}"); + } + } else { + eprintln!("TEST: bad result {result:?}"); + } + None + }) + .collect::>(); + 'next_file: for file in files { + for name in &found { + if file == name { + continue 'next_file; + } + } + bail!("Failed to find file {file:?}, found {found:?}"); + } + Ok(()) +} + +/// Command `generate` works. +#[test] +fn generate() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["generate", "--svd", "example.svd"]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["lib.rs", "device.x"])?; + Ok(()) +} + +/// Command `extract-all` works. +#[test] +fn extract_all() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["extract-all", "--svd", "example.svd", "--output", "."]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["TIMER0.yaml"])?; + if false { + // FIXME this command does not support derivedFrom + expect_files_in(tmp.path(), &["TIMER1.yaml", "TIMER2.yaml"])?; + } + Ok(()) +} + +/// Command `extract-peripheral` works. +#[test] +fn extract_peripheral() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + for peripheral in ["TIMER0", "TIMER1", "TIMER2"] { + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args([ + "extract-peripheral", + "--svd", + "example.svd", + "--peripheral", + peripheral, + ]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + // FIXME the block name of TIMER1 and TIMER2 say TIMER0 + } + Ok(()) +} + +/// Command `transform` works. +#[test] +fn transform() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + copy_transform_to(tmp.path(), "transform.yaml")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["extract-all", "--svd", "example.svd", "--output", "."]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["TIMER0.yaml"])?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args([ + "transform", + "--input", + "TIMER0.yaml", + "--output", + "output.yaml", + "--transform", + "transform.yaml", + ]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["output.yaml"])?; + Ok(()) +} + +/// Command `fmt` works. +#[test] +fn fmt() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["extract-all", "--svd", "example.svd", "--output", "."]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["TIMER0.yaml"])?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["fmt", "TIMER0.yaml"]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + Ok(()) +} + +/// Command `check` works. +#[test] +fn check() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["extract-all", "--svd", "example.svd", "--output", "."]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["TIMER0.yaml"])?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["check", "TIMER0.yaml", "--allow-register-overlap"]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + Ok(()) +} + +/// Command `gen-block` works. +#[test] +fn gen_block() -> Result<()> { + let tmp = create_temporary_dir()?; + copy_svd_to(tmp.path(), "example.svd")?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args(["extract-all", "--svd", "example.svd", "--output", "."]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["TIMER0.yaml"])?; + let mut cmd = cargo_bin_cmd!(); + cmd.current_dir(tmp.path()); + cmd.args([ + "gen-block", + "--input", + "TIMER0.yaml", + "--output", + "timer0.rs", + ]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + expect_files_in(tmp.path(), &["timer0.rs"])?; + Ok(()) +} + +/// Command `help` works. +#[test] +fn help() -> Result<()> { + let mut cmd = cargo_bin_cmd!(); + cmd.args(["help"]); + cmd.assert() + .append_context("cmd", format!("{cmd:?}")) + .success() + .print(); + Ok(()) +} diff --git a/tests/transform.yaml b/tests/transform.yaml new file mode 100644 index 0000000..e69de29