feat: Move ID tests into their own crate with a CLI (#177)

* fix: ID tests not in correct directory

* refactor: Move game-id test logic into its own crate

* id-tests: Add CLI that reads JSON input

* id-tests: Update crate docs

* Remove node ID test

* id-tests: Don't try to parse unneeded info

* id-tests: Enable cli feature by default
This commit is contained in:
Tom 2024-01-10 23:31:02 +00:00 committed by GitHub
parent 88bf996a5e
commit b248a7661e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 113 deletions

View file

@ -1,5 +1,5 @@
[workspace]
members = ["crates/cli", "crates/lib"]
members = ["crates/cli", "crates/lib", "crates/id-tests"]
# Edition 2021, uses resolver = 2
resolver = "2"

View file

@ -0,0 +1,29 @@
[package]
name = "gamedig-id-tests"
version = "0.0.1"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Test if IDs match the gamedig rules"
homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
rust-version = "1.65.0"
[features]
cli = ["dep:serde_json", "dep:serde"]
default = ["cli"]
[[bin]]
name = "gamedig-id-tests"
required-features = ["cli"]
[dependencies]
number_to_words = "0.1"
roman_numeral = "0.1"
serde_json = { version = "1", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }

View file

@ -1,10 +1,7 @@
#![cfg(all(test, feature = "game_defs"))]
use std::collections::HashMap;
use std::{collections::HashMap, fs, io::Read};
use gamedig::GAMES;
use utils::*;
mod utils;
use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IDRule {
@ -384,42 +381,9 @@ pub fn test_game_name_rules<'a, I: Iterator<Item = (&'a str, &'a str)>>(games: I
wrong_ids
}
#[test]
fn check_definitions_match_name_rules() {
let wrong = test_game_name_rules(GAMES.entries().map(|(id, game)| (id.to_owned(), game.name)));
assert!(wrong.is_empty());
}
#[test]
#[ignore = "Don't test node by default"]
fn check_node_definitions_match_name_rules() {
let mut file = fs::OpenOptions::new()
.read(true)
.open("./node-gamedig/games.txt")
.unwrap();
let mut text = String::new();
file.read_to_string(&mut text).unwrap();
let games = text
.split('\n')
.map(|line| line.trim())
.filter(|line| !line.starts_with('#') && !line.is_empty())
.filter_map(|line| {
let parts: Vec<_> = line.splitn(3, '|').collect();
if parts.len() > 1 {
Some((parts[0].split(',').next().unwrap(), parts[1]))
} else {
None
}
});
let wrong = test_game_name_rules(games);
assert!(wrong.is_empty());
}
fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
pub fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
#[cfg(test)]
mod id_tests {
use super::{test_game_name_rules, test_single_game_rule};
#[test]
@ -485,72 +449,3 @@ mod id_tests {
assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty());
}
}
mod utils {
/// Split a str when characters swap between being digits and not digits.
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![];
}
let mut parts = Vec::with_capacity(text.len());
let mut current = Vec::with_capacity(text.len());
let mut iter = text.chars();
let c = iter.next().unwrap();
let mut last_was_numeric = c.is_ascii_digit();
current.push(c);
for c in iter {
if c.is_ascii_digit() == last_was_numeric {
current.push(c);
} else {
parts.push(current.iter().collect());
current.clear();
current.push(c);
last_was_numeric = !last_was_numeric;
}
}
parts.push(current.into_iter().collect());
parts
}
#[test]
fn split_correctly() {
assert_eq!(
split_on_switch_between_alpha_numeric("2D45A"),
&["2", "D", "45", "A"]
);
}
#[test]
fn split_symbol_broken_numbers() {
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
}
/// Extract parts at end of string enclosed in brackets.
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
if let Some(text) = text.strip_suffix(')') {
if let Some((text, extra)) = text.rsplit_once('(') {
return (text, Some(extra));
}
}
(text, None)
}
#[test]
fn extract_brackets_correctly() {
assert_eq!(
extract_bracketed_suffix("no brackets here"),
("no brackets here", None)
);
assert_eq!(
extract_bracketed_suffix("Game name (with protocol here)"),
("Game name ", Some("with protocol here"))
);
}
}

View file

@ -0,0 +1,31 @@
#![cfg(feature = "cli")]
use std::collections::HashMap;
/// Format for input games (the same as used in node-gamedig/lib/games.js).
type GamesInput = HashMap<String, Game>;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
struct Game {
name: String,
}
use gamedig_id_tests::test_game_name_rules;
fn main() {
let games: GamesInput = if let Some(file) = std::env::args_os().skip(1).next() {
let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
serde_json::from_reader(file).unwrap()
} else {
serde_json::from_reader(std::io::stdin().lock()).unwrap()
};
let failed = test_game_name_rules(
games
.iter()
.map(|(key, game)| (key.as_str(), game.name.as_str())),
);
assert!(failed.is_empty());
}

View file

@ -0,0 +1,66 @@
/// Split a str when characters swap between being digits and not digits.
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![];
}
let mut parts = Vec::with_capacity(text.len());
let mut current = Vec::with_capacity(text.len());
let mut iter = text.chars();
let c = iter.next().unwrap();
let mut last_was_numeric = c.is_ascii_digit();
current.push(c);
for c in iter {
if c.is_ascii_digit() == last_was_numeric {
current.push(c);
} else {
parts.push(current.iter().collect());
current.clear();
current.push(c);
last_was_numeric = !last_was_numeric;
}
}
parts.push(current.into_iter().collect());
parts
}
#[test]
fn split_correctly() {
assert_eq!(
split_on_switch_between_alpha_numeric("2D45A"),
&["2", "D", "45", "A"]
);
}
#[test]
fn split_symbol_broken_numbers() {
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
}
/// Extract parts at end of string enclosed in brackets.
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
if let Some(text) = text.strip_suffix(')') {
if let Some((text, extra)) = text.rsplit_once('(') {
return (text, Some(extra));
}
}
(text, None)
}
#[test]
fn extract_brackets_correctly() {
assert_eq!(
extract_bracketed_suffix("no brackets here"),
("no brackets here", None)
);
assert_eq!(
extract_bracketed_suffix("Game name (with protocol here)"),
("Game name ", Some("with protocol here"))
);
}

View file

@ -38,8 +38,7 @@ phf = { version = "0.11", optional = true, features = ["macros"] }
clap = { version = "4.1.11", optional = true, features = ["derive"] }
[dev-dependencies]
number_to_words = "0.1"
roman_numeral = "0.1"
gamedig-id-tests = { path = "../id-tests", no-default-features = true }
# Examples
[[example]]

View file

@ -0,0 +1,11 @@
#![cfg(all(test, feature = "game_defs"))]
use gamedig::GAMES;
use gamedig_id_tests::test_game_name_rules;
#[test]
fn check_definitions_match_name_rules() {
let wrong = test_game_name_rules(GAMES.entries().map(|(id, game)| (id.to_owned(), game.name)));
assert!(wrong.is_empty());
}