Skip to content

Commit 5f9bfb4

Browse files
authored
cleanup: CI / dependabot / tests (#1)
1 parent d99bd03 commit 5f9bfb4

6 files changed

Lines changed: 151 additions & 23 deletions

File tree

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# Dependabot configuration file
3+
#
4+
5+
version: 2
6+
updates:
7+
- package-ecosystem: "cargo"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"
11+
- package-ecosystem: "github-actions"
12+
directory: "/"
13+
schedule:
14+
interval: "weekly"

.github/workflows/release.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ permissions: {}
1313
jobs:
1414
create-release:
1515
runs-on: ubuntu-latest
16+
if: github.repository_owner == 'oxidecomputer'
1617
permissions:
1718
# for creating the GitHub release
1819
contents: write
@@ -21,8 +22,16 @@ jobs:
2122
with:
2223
persist-credentials: false
2324
- name: Install Rust
24-
uses: dtolnay/rust-toolchain@stable
25-
- run: cargo publish -p codespace
25+
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
26+
- name: Check that the tag matches the crate version
27+
run: |
28+
version=$(cargo metadata --no-deps --format-version 1 \
29+
| jq -r '.packages[0].version')
30+
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
31+
echo "tag $GITHUB_REF_NAME does not match crate version $version" >&2
32+
exit 1
33+
fi
34+
- run: cargo publish -p codespace --locked
2635
env:
2736
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
2837
- uses: taiki-e/create-gh-release-action@72d65cee1f8033ef0c8b5d79eaf0c45c7c578ce3 # v1

.github/workflows/rust.yml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,62 @@ on:
88
branches: [ main ]
99
pull_request:
1010
branches: [ main ]
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: true
1119

1220
jobs:
1321
check-style:
1422
runs-on: ubuntu-latest
1523
steps:
16-
- uses: actions/checkout@v4
24+
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
1725
- name: Report cargo version
1826
run: cargo --version
1927
- name: Report rustfmt version
2028
run: cargo fmt -- --version
2129
- name: Check style
2230
run: cargo fmt -- --check
2331

32+
clippy:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
36+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
37+
- name: Report clippy version
38+
run: cargo clippy --version
39+
- name: Run clippy
40+
run: cargo clippy --locked --all-targets -- -D warnings
41+
42+
docs:
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
46+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
47+
- name: Build documentation
48+
run: cargo doc --locked --no-deps
49+
env:
50+
RUSTDOCFLAGS: -D warnings
51+
2452
build-and-test:
2553
runs-on: ${{ matrix.os }}
2654
strategy:
2755
matrix:
2856
os: [ ubuntu-latest, windows-latest, macos-latest ]
57+
# 1.97 is the MSRV
58+
rust-version: [ stable, "1.97" ]
2959
steps:
30-
- uses: actions/checkout@v4
60+
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
61+
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
62+
with:
63+
toolchain: ${{ matrix.rust-version }}
64+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
65+
- name: Report rustc version
66+
run: rustc --version
3167
- name: Build
3268
run: cargo build --locked --tests --verbose
3369
- name: Run tests

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
name = "codespace"
33
version = "0.0.1-alpha.1"
44
edition = "2021"
5+
rust-version = "1.97"
56
license = "Apache-2.0"
67
repository = "https://github.com/oxidecomputer/codespace"
7-
description = "A structured container for generated Rust code"
8+
description = "Structural scratch space for generated Rust code"
9+
exclude = [".github", "rust-toolchain.toml"]
810

911
[dependencies]
1012
proc-macro2 = "1.0.104"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![codespace on crates.io](https://img.shields.io/crates/v/codespace)](https://crates.io/crates/codespace)
44
[![Documentation (latest release)](https://img.shields.io/badge/docs-latest%20version-brightgreen.svg)](https://docs.rs/codespace)
5-
[![License](https://img.shields.io/badge/license-Apache-green.svg)](LICENSE)
5+
[![License](https://img.shields.io/badge/license-Apache-green.svg)](https://github.com/oxidecomputer/codespace/blob/main/LICENSE)
66

77
Structural scratch space for generated Rust code
88

@@ -54,5 +54,5 @@ details.
5454

5555
## Notes
5656

57-
- Pre-publication; API unstable.
57+
- Early alpha; API unstable.
5858
- Part of the typify/progenitor code-generation stack.

src/lib.rs

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
//! Within each [`Mod`], items are emitted in sort-key order and submodules
4747
//! in alphabetical order by name.
4848
49+
#![forbid(unsafe_code)]
50+
#![warn(missing_docs, missing_debug_implementations)]
51+
4952
use std::{
5053
collections::{btree_map::Entry, BTreeMap},
5154
path::{Path, PathBuf},
@@ -67,7 +70,7 @@ fn validate_mod_name(name: &str, full: &str) {
6770
// syn must accept `gen` (an identifier before edition 2024), but our
6871
// generated code may land in a 2024 crate, so we reject it ourselves.
6972
if name == "gen" {
70-
panic!("module name {:?} (in {:?}) is a Rust keyword", name, full);
73+
panic!("module name {name:?} (in {full:?}) is a Rust keyword");
7174
}
7275
if !name.starts_with("r#") && syn::parse_str::<syn::Ident>(name).is_ok() {
7376
return;
@@ -84,12 +87,11 @@ fn validate_mod_name(name: &str, full: &str) {
8487
)
8588
});
8689
if is_keyword {
87-
panic!("module name {:?} (in {:?}) is a Rust keyword", name, full);
90+
panic!("module name {name:?} (in {full:?}) is a Rust keyword");
8891
}
8992
panic!(
90-
"module name {:?} (in {:?}) is not a valid Rust identifier \
93+
"module name {name:?} (in {full:?}) is not a valid Rust identifier \
9194
(raw identifiers are not supported)",
92-
name, full,
9395
);
9496
}
9597

@@ -505,7 +507,7 @@ impl Mod {
505507
#vis mod #ident;
506508
});
507509
let (file, child_dir) = if m.mods.is_empty() {
508-
(dir.join(format!("{}.rs", name)), dir.to_path_buf())
510+
(dir.join(format!("{name}.rs")), dir.to_path_buf())
509511
} else {
510512
let child_dir = dir.join(&name);
511513
(child_dir.join("mod.rs"), child_dir)
@@ -622,7 +624,7 @@ mod tests {
622624
}
623625

624626
#[test]
625-
#[should_panic]
627+
#[should_panic(expected = "is not a valid Rust identifier")]
626628
fn invalid_mod_name_panics() {
627629
let mut cs = Codespace::default();
628630
cs.add_item("not-valid-ident::key", quote! {});
@@ -671,7 +673,7 @@ mod tests {
671673
}
672674

673675
#[test]
674-
#[should_panic]
676+
#[should_panic(expected = "is not a valid Rust identifier")]
675677
fn get_mod_invalid_ident_panics() {
676678
let mut cs = Codespace::default();
677679
cs.get_root_mod().get_mod("not-valid");
@@ -751,9 +753,9 @@ mod tests {
751753
m.add_attr(quote! { allow(dead_code) });
752754
m.add_item("f", quote! { pub fn f() {} });
753755
let out = no_ws(&cs.into_stream().to_string());
754-
assert!(out.contains(r##"#[doc="Helperfunctions."]"##));
756+
assert!(out.contains(r#"#[doc="Helperfunctions."]"#));
755757
// Metadata appears immediately before the mod, in outer form.
756-
assert!(out.contains(r##"#[allow(dead_code)]pubmodhelpers{"##));
758+
assert!(out.contains(r#"#[allow(dead_code)]pubmodhelpers{"#));
757759
assert!(!out.contains("#!"));
758760
}
759761

@@ -815,11 +817,56 @@ mod tests {
815817
let mut cs = Codespace::default();
816818
cs.get_root_mod().add_mod("m", a);
817819
let out = no_ws(&cs.into_stream().to_string());
818-
assert!(out.contains(r##"#[doc="First."]#[doc=""]#[doc="Second."]"##));
820+
assert!(out.contains(r#"#[doc="First."]#[doc=""]#[doc="Second."]"#));
819821
assert!(out.contains("#[allow(dead_code)]#[allow(unused)]"));
820822
assert!(out.contains("pub(crate)modm"));
821823
}
822824

825+
#[test]
826+
fn replace_mod_clobbers_existing() {
827+
let mut root = Mod::default();
828+
// First insertion: nothing to return.
829+
let mut m1 = Mod::default();
830+
m1.add_item("f", quote! { pub fn f() {} });
831+
assert!(root.replace_mod("client", m1).is_none());
832+
// Replacement returns the previous module and clobbers it--no
833+
// merging, in contrast to add_mod.
834+
let mut m2 = Mod::default();
835+
m2.add_item("g", quote! { pub fn g() {} });
836+
let prev = root.replace_mod("client", m2).unwrap();
837+
assert!(prev.items.contains_key("f"));
838+
let mut cs = Codespace::default();
839+
*cs.get_root_mod() = root;
840+
let out = cs.into_stream().to_string();
841+
assert_eq!(out.matches("mod client").count(), 1);
842+
assert!(out.contains("fn g"));
843+
assert!(!out.contains("fn f"));
844+
}
845+
846+
#[test]
847+
#[should_panic(expected = "is a Rust keyword")]
848+
fn replace_mod_keyword_panics() {
849+
let mut root = Mod::default();
850+
root.replace_mod("type", Mod::default());
851+
}
852+
853+
#[test]
854+
fn into_root_mod_mounts_in_another_codespace() {
855+
// Generate a subsystem in its own Codespace, then mount it as a
856+
// submodule of a larger one.
857+
let mut inner = Codespace::default();
858+
inner.add_item("Foo", quote! { pub struct Foo; });
859+
inner.add_item("defaults::f", quote! { pub fn f() {} });
860+
let mut outer = Codespace::default();
861+
outer.add_item("Bar", quote! { pub struct Bar; });
862+
outer.add_mod("subsystem", inner.into_root_mod());
863+
let out = no_ws(&outer.into_stream().to_string());
864+
assert!(out.contains("pubstructBar"));
865+
assert!(out.contains("pubmodsubsystem{"));
866+
assert!(out.contains("pubstructFoo"));
867+
assert!(out.contains("pubmoddefaults{"));
868+
}
869+
823870
#[test]
824871
fn add_mod_inserts_new() {
825872
let mut cs = Codespace::default();
@@ -912,14 +959,35 @@ mod tests {
912959
// Docs and attrs are outer, at the declaration site, immediately
913960
// before the visibility and declaration.
914961
assert!(
915-
lib.contains(r##"#[doc="Helperfunctions."]#[allow(dead_code)]pub(crate)modhelpers;"##)
962+
lib.contains(r#"#[doc="Helperfunctions."]#[allow(dead_code)]pub(crate)modhelpers;"#)
916963
);
917964
// Nothing of the metadata leaks into the child file.
918965
let helpers = no_ws(&files[Path::new("helpers.rs")].to_string());
919966
assert!(!helpers.contains("doc"));
920967
assert!(!helpers.contains("allow"));
921968
}
922969

970+
#[test]
971+
fn into_files_non_leaf_decl_carries_metadata() {
972+
// Like into_files_decl_carries_metadata, but for a module with
973+
// children: metadata belongs on the declaration in the parent, not
974+
// in the child's mod.rs.
975+
let mut cs = Codespace::default();
976+
let m = cs.get_root_mod().get_mod("outer");
977+
m.set_visibility(Visibility::Private);
978+
m.add_docs("Outer module.");
979+
m.add_attr(quote! { allow(dead_code) });
980+
m.get_mod("inner").add_item("f", quote! { pub fn f() {} });
981+
let files = cs.into_files();
982+
let lib = no_ws(&files[Path::new("lib.rs")].to_string());
983+
assert!(lib.contains(r#"#[doc="Outermodule."]#[allow(dead_code)]modouter;"#));
984+
assert!(!lib.contains("pubmodouter"));
985+
// The child's own file holds only its contents and its child's
986+
// declaration.
987+
let outer = no_ws(&files[Path::new("outer/mod.rs")].to_string());
988+
assert_eq!(outer, "pubmodinner;");
989+
}
990+
923991
#[test]
924992
fn into_files_root_meta_is_inner() {
925993
let mut cs = Codespace::default();
@@ -928,7 +996,7 @@ mod tests {
928996
cs.add_item("Foo", quote! { pub struct Foo; });
929997
let files = cs.into_files();
930998
let lib = no_ws(&files[Path::new("lib.rs")].to_string());
931-
assert!(lib.starts_with(r##"#![doc="Generatedcode."]#![allow(clippy::all)]"##));
999+
assert!(lib.starts_with(r#"#![doc="Generatedcode."]#![allow(clippy::all)]"#));
9321000
}
9331001

9341002
#[test]
@@ -971,7 +1039,7 @@ mod tests {
9711039
// Lines within one call stay in one paragraph; a blank doc line
9721040
// separates calls.
9731041
assert!(out.contains(
974-
r##"#[doc="Firstparagraph."]#[doc="Stillthefirst."]#[doc=""]#[doc="Secondparagraph."]"##
1042+
r#"#[doc="Firstparagraph."]#[doc="Stillthefirst."]#[doc=""]#[doc="Secondparagraph."]"#
9751043
));
9761044
}
9771045

@@ -982,9 +1050,8 @@ mod tests {
9821050
cs.get_root_mod().add_attr(quote! { allow(clippy::all) });
9831051
cs.add_item("Foo", quote! { pub struct Foo; });
9841052
let out = no_ws(&cs.into_stream().to_string());
985-
assert!(out.starts_with(
986-
r##"#![doc="Generatedcode."]#![doc="Donotedit."]#![allow(clippy::all)]"##
987-
));
1053+
assert!(out
1054+
.starts_with(r#"#![doc="Generatedcode."]#![doc="Donotedit."]#![allow(clippy::all)]"#));
9881055
assert!(out.contains("pubstructFoo"));
9891056
}
9901057
}

0 commit comments

Comments
 (0)