Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ jobs:
run: cargo test --doc --workspace

- if: runner.os == 'macOS'
run: cargo run -p cp-mac --example probe
run: cargo run -p cp-mac --example mac_probe
- name: The probe reads a real clipboard, so leave something on it
if: runner.os == 'Windows'
shell: pwsh
run: Set-Clipboard -Value "cp-probe"
- if: runner.os == 'Windows'
run: cargo run -p cp-win --example probe
run: cargo run -p cp-win --example win_probe

lint:
name: fmt
Expand Down Expand Up @@ -112,6 +112,7 @@ jobs:
shared-key: cross
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo check --target aarch64-apple-darwin -p cp-mac -p cp-mac-sys
- run: cargo check --target aarch64-apple-darwin -p cp-mac-sys --all-targets

deny:
name: advisories + licences
Expand Down Expand Up @@ -143,9 +144,9 @@ jobs:
: > "app/src-tauri/binaries/cp-panel-$triple"

- run: cargo llvm-cov --no-report --workspace
- run: cargo llvm-cov --no-report run -p cp-mac --example probe
- run: cargo llvm-cov report --lcov --output-path lcov-rust.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --fail-under-lines 90 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov --no-report run -p cp-mac --example mac_probe
- run: cargo llvm-cov report --lcov --output-path lcov-rust.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)(_test)?\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --fail-under-lines 90 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)(_test)?\.rs|src-tauri[/\]src[/\]'

- uses: actions/setup-node@v7
with:
Expand Down Expand Up @@ -191,9 +192,9 @@ jobs:
- name: The probe reads a real clipboard, so leave something on it
shell: pwsh
run: Set-Clipboard -Value "cp-probe"
- run: cargo llvm-cov --no-report run -p cp-win --example probe
- run: cargo llvm-cov report --lcov --output-path lcov-win.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --fail-under-lines 95 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov --no-report run -p cp-win --example win_probe
- run: cargo llvm-cov report --lcov --output-path lcov-win.info --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)(_test)?\.rs|src-tauri[/\]src[/\]'
- run: cargo llvm-cov report --fail-under-lines 95 --summary-only --ignore-filename-regex 'cp-panel[/\]src[/\](app|main|measure)(_test)?\.rs|src-tauri[/\]src[/\]'
- name: Every path is relative to the repository, which is what Sonar reads
if: always()
shell: bash
Expand Down Expand Up @@ -253,6 +254,6 @@ jobs:
-Dsonar.rust.lcov.reportPaths=lcov-rust.info,lcov-win.info
-Dsonar.javascript.lcov.reportPaths=app/coverage/lcov.info
-Dsonar.rust.clippy.enabled=false
-Dsonar.coverage.exclusions=app/src/main.tsx,app/src-tauri/src/**,crates/cp-panel/src/main.rs,crates/cp-panel/src/app.rs,crates/cp-panel/src/measure.rs
-Dsonar.coverage.exclusions=app/src/main.tsx,app/src-tauri/src/**,crates/cp-panel/src/main.rs,crates/cp-panel/src/app.rs,crates/cp-panel/src/app_test.rs,crates/cp-panel/src/measure.rs,crates/cp-panel/src/measure_test.rs
-Dsonar.qualitygate.wait=${{ github.event_name == 'pull_request' }}
-Dsonar.qualitygate.timeout=300
22 changes: 22 additions & 0 deletions .github/workflows/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ jobs:
echo "::error::no block comments either"; exit 1
fi

- name: The tests live beside the file, not inside it
run: python3 scripts/oversized.py --inline

- name: No two crates name an example the same
run: |
set -euo pipefail
dirs=$(find . -type d -name examples -not -path './target/*' \
-not -path '*/node_modules/*' | sort)
said=$(for one in $dirs; do
find "$one" -mindepth 1 -maxdepth 1 -name '*.rs' -exec basename {} .rs \;
find "$one" -mindepth 2 -maxdepth 2 -name 'main.rs' -printf '%h\n' \
| xargs -r -n1 basename
done | sort)
twice=$(printf '%s\n' "$said" | uniq -d)
if [ -n "$twice" ]; then
echo "::error::two crates name an example the same, and cargo links every example to \
target/<profile>/examples/<name>, so they are one binary and the two link steps \
race: $(echo "$twice" | tr '\n' ' ')"
exit 1
fi
echo "every example has a name of its own: $(echo "$said" | tr '\n' ' ')"

- name: No file or function grows past what a person can hold
run: python3 scripts/oversized.py

Expand Down
32 changes: 28 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,33 @@ In many ways! Code is just one of them:
2. **Create a branch** from `main` (`git checkout -b feature/my-improvement`)
3. **Make your changes** following our style:
- Clean and minimalist code
- Code, identifiers and comments in English
- Tests when appropriate
4. **Make sure** all tests pass:
- Code and identifiers in English
- No comments, with no exception. CI refuses any `//` or `/*` in `crates` and
`app/src-tauri/src`: a name that needs a comment is the wrong name
- Tests when appropriate, and **in a file of their own** — see below
4. **Where a test goes.** Never inside the file it tests. Each file keeps its
tests in a sibling, declared at the bottom of the file under test:

```rust
#[cfg(test)]
#[path = "watch_test.rs"]
mod tests;
```

The sibling lives in the same directory, holds the body without the
`mod tests { }` wrapper, and is still a child module — `use super::*` and
access to private items work exactly as if it were inline. Its name is the
file plus `_test.rs`; when one file has several test modules, the name says
which one it is and still ends in `_test.rs`: `store_listing_test.rs`,
`kind_borders_test.rs`, `waking_windows_test.rs`. The suffix is not decorative:
the ceiling refuses a declaration pointing at a name without it, so a sibling
named anything else is measured as production code and fails at 1500 lines.
The declaration goes **last** in the file, after the code — clippy asked for
that while the module was inline and cannot see it any more, so
`scripts/oversized.py --inline` does. A test module written inside the file
fails CI, as does a `*_test.rs` that nothing declares.

5. **Make sure** all tests pass:

```sh
python3 scripts/sidecar.py --debug # the app carries the panel; build it first
Expand All @@ -63,7 +87,7 @@ In many ways! Code is just one of them:
cd app && npm ci && npm run lint && npm test
```

5. **Open a Pull Request** to `main`
6. **Open a Pull Request** to `main`

### Translate

Expand Down
22 changes: 4 additions & 18 deletions app/src-tauri/src/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,6 @@ fn bring(from: &std::path::Path, at: i64) -> Result<cp_store::Brought, String> {
cp_store::backup::bring(from, &store, at).map_err(|why| why.to_string())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn the_history_a_backup_speaks_for_is_the_one_the_panel_writes() {
let Ok(db) = history() else {
return;
};
let dir = crate::settings::folder().expect("a folder");
assert!(db.starts_with(&dir));
assert_eq!(
db.file_name().and_then(|it| it.to_str()),
Some("history.db")
);
}
}

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Crossed {
Expand Down Expand Up @@ -221,3 +203,7 @@ pub fn drop_former() -> Result<Swept, String> {
bytes: swept.bytes,
})
}

#[cfg(test)]
#[path = "backup_test.rs"]
mod tests;
14 changes: 14 additions & 0 deletions app/src-tauri/src/backup_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use super::*;

#[test]
fn the_history_a_backup_speaks_for_is_the_one_the_panel_writes() {
let Ok(db) = history() else {
return;
};
let dir = crate::settings::folder().expect("a folder");
assert!(db.starts_with(&dir));
assert_eq!(
db.file_name().and_then(|it| it.to_str()),
Some("history.db")
);
}
22 changes: 2 additions & 20 deletions app/src-tauri/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,5 @@ pub fn keys(app: tauri::AppHandle) -> Keys {
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn what_the_settings_file_ships_with_is_a_combination_the_system_knows() {
assert!(cp_config::SHORTCUT.parse::<Shortcut>().is_ok());
}

#[test]
fn a_combination_nobody_could_press_is_refused_before_the_system_sees_it() {
assert!("".parse::<Shortcut>().is_err());
assert!("Ctrl+".parse::<Shortcut>().is_err());
}

#[test]
fn nothing_is_bound_until_the_system_says_yes() {
let bound = Bound::default();
assert!(bound.0.lock().expect("unpoisoned").is_none());
}
}
#[path = "keys_test.rs"]
mod tests;
18 changes: 18 additions & 0 deletions app/src-tauri/src/keys_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use super::*;

#[test]
fn what_the_settings_file_ships_with_is_a_combination_the_system_knows() {
assert!(cp_config::SHORTCUT.parse::<Shortcut>().is_ok());
}

#[test]
fn a_combination_nobody_could_press_is_refused_before_the_system_sees_it() {
assert!("".parse::<Shortcut>().is_err());
assert!("Ctrl+".parse::<Shortcut>().is_err());
}

#[test]
fn nothing_is_bound_until_the_system_says_yes() {
let bound = Bound::default();
assert!(bound.0.lock().expect("unpoisoned").is_none());
}
24 changes: 2 additions & 22 deletions app/src-tauri/src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,5 @@ fn clock() -> String {
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn the_log_sits_where_the_privacy_note_says_it_does() {
let path = where_to();
assert_eq!(
path.file_name().and_then(|it| it.to_str()),
Some("cp-gui.log")
);
if let Some(dir) = crate::settings::folder() {
assert!(path.starts_with(dir));
}
}

#[test]
fn the_clock_reads_as_hours_minutes_and_seconds() {
let said = clock();
assert_eq!(said.len(), 8);
assert!(said.chars().filter(|one| *one == ':').count() == 2);
}
}
#[path = "note_test.rs"]
mod tests;
20 changes: 20 additions & 0 deletions app/src-tauri/src/note_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use super::*;

#[test]
fn the_log_sits_where_the_privacy_note_says_it_does() {
let path = where_to();
assert_eq!(
path.file_name().and_then(|it| it.to_str()),
Some("cp-gui.log")
);
if let Some(dir) = crate::settings::folder() {
assert!(path.starts_with(dir));
}
}

#[test]
fn the_clock_reads_as_hours_minutes_and_seconds() {
let said = clock();
assert_eq!(said.len(), 8);
assert!(said.chars().filter(|one| *one == ':').count() == 2);
}
11 changes: 2 additions & 9 deletions app/src-tauri/src/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,5 @@ pub enum Said {
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn a_sidecar_starts_with_nobody_on_the_other_end() {
let sidecar = Sidecar::default();
assert!(sidecar.0.lock().expect("unpoisoned").is_none());
}
}
#[path = "panel_test.rs"]
mod tests;
7 changes: 7 additions & 0 deletions app/src-tauri/src/panel_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use super::*;

#[test]
fn a_sidecar_starts_with_nobody_on_the_other_end() {
let sidecar = Sidecar::default();
assert!(sidecar.0.lock().expect("unpoisoned").is_none());
}
52 changes: 2 additions & 50 deletions app/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,53 +110,5 @@ pub fn surface<R: Runtime>(app: &AppHandle<R>) {
}

#[cfg(test)]
mod tests {
use super::spanish;
use tauri::image::Image;

const WINDOWS: &[u8] = include_bytes!("../icons/tray/windows-32.png");
const MACOS: &[u8] = include_bytes!("../icons/tray/macos@2x.png");

fn ink(png: &[u8]) -> (u32, usize) {
let art = Image::from_bytes(png).expect("the icon is a png");
let pixels = art.rgba();
let (sum, seen) =
pixels
.as_chunks::<4>()
.0
.iter()
.fold((0u64, 0usize), |(sum, seen), px| {
if px[3] < 128 {
return (sum, seen);
}
let grey =
(px[0] as u64 * 299 + px[1] as u64 * 587 + px[2] as u64 * 114) / 1000;
(sum + grey, seen + 1)
});
assert!(seen > 0, "the icon is transparent through and through");
((sum / seen as u64) as u32, seen)
}

#[test]
fn the_bar_icon_on_macos_is_pale_because_the_system_paints_it_itself() {
let (grey, _) = ink(MACOS);
assert!(grey > 200, "the macOS template has to be pale: {grey}");
}

#[test]
fn only_a_locale_that_starts_with_en_gets_english() {
assert!(!spanish(Some("en")));
assert!(!spanish(Some("en-GB")));
assert!(!spanish(Some("EN-us")));
assert!(spanish(Some("es")));
assert!(spanish(Some("es-CL")));
assert!(spanish(Some("pt-BR")));
}

#[test]
fn the_windows_icon_is_dark_enough_for_a_light_bar() {
let (grey, seen) = ink(WINDOWS);
assert!(seen > 64, "the Windows icon has hardly any ink: {seen}");
assert!(grey < 200, "on a light bar it would not show: {grey}");
}
}
#[path = "tray_test.rs"]
mod tests;
Loading
Loading