The Rust worker is the product: edge/, built and checked in CI
User: Rust is the product (option a). tools/edge-bench-rs moves to edge/
(crate edge; binaries edge-node, the worker, and edge-bench, the replay).
CI's new job edge runs cargo test and edge/check-fixtures.sh: both
RangeIndexers on three committed captures (edge/fixtures, 25k overview
lines each of a.b.models, a.b.ath and a.b.boneless in 5 chunks, 3.5 MB),
compared field by field with check.ts; deploy waits for it. The
TypeScript RangeIndexer stays as check.ts's reference. README: what it
is now; the benchmark findings as history.
Random names: looksRandom missed long tokens of capitals and digits
("51294P1CUKR2R35X", release 74539068, readable and a Newznab title).
12+ capitals and digits switching 6+ times between them are random
(COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice); both
implementations, identical on the fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48f4e982f9
commit
5109481d67
38 files changed
+138
-54
No files matched your search
+1
-1
@@ -3,4 +3,4 @@ build
|
||||
tmp
|
||||
.env
|
||||
.git
|
||||
tools/edge-bench-rs/target
|
||||
edge/target
|
||||
@@ -66,8 +66,28 @@ jobs:
|
||||
- name: Test
|
||||
run: node ace test
|
||||
|
||||
# The edge worker (edge/, Rust): its tests, and the Rust and TypeScript
|
||||
# RangeIndexers compared on the committed captures (edge/fixtures).
|
||||
edge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Rust toolchain
|
||||
run: |
|
||||
command -v cargo || curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: cargo test
|
||||
run: cargo test --release --manifest-path edge/Cargo.toml
|
||||
- name: Rust and TypeScript identical on the fixtures
|
||||
run: edge/check-fixtures.sh
|
||||
|
||||
deploy:
|
||||
needs: test
|
||||
needs: [test, edge]
|
||||
if: github.ref == 'refs/heads/rebuild'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Voyager 2's images (deploy/compose.yaml):
|
||||
# --target app the AdonisJS app: web server, coordinator, migrations, ace commands
|
||||
# --target edge the Rust edge worker (tools/edge-bench-rs, bin edge-node)
|
||||
# --target edge the Rust edge worker (edge/, bin edge-node)
|
||||
|
||||
# Node is pinned as in CI (.nvmrc): on 24.21, jsonschema 1.5.0 (used by Ace to
|
||||
# validate commands) throws "Invalid URL" on $ref, so no ace command starts.
|
||||
@@ -29,7 +29,7 @@ CMD ["node", "ace", "list"]
|
||||
# The Rust worker. Registry and target dir are build caches, so a rebuild only compiles what changed.
|
||||
FROM rust:1-bookworm AS edge-build
|
||||
WORKDIR /src
|
||||
COPY tools/edge-bench-rs ./
|
||||
COPY edge ./
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/src/target \
|
||||
cargo build --release --locked --bin edge-node \
|
||||
|
||||
+2
-2
@@ -119,7 +119,7 @@ dropping garbage; 28 TB of compressed overview traffic. Throughput goal:
|
||||
2. **Edge worker** (node, TypeScript reusing `lib/`). Measured (`edge:bench`,
|
||||
2026-10-09, after two rounds of work): ~200–215 k headers/s per core on
|
||||
recent a.b.boneless, ~320–340 k on a 2013 a.b.ath range, ~0.7 GB peak
|
||||
per process. **Rust** (`tools/edge-bench-rs`, the same RangeIndexer path
|
||||
per process. **Rust** (`edge/`, the same RangeIndexer path
|
||||
over the same captures; `check.ts` finds every output identical, the
|
||||
decoded payloads included): 3.5–3.8× less CPU (boneless 2.9 vs 11.1
|
||||
CPU-s per 2 M headers, ath 2.15 vs 7.5) and a third of the memory
|
||||
@@ -130,7 +130,7 @@ dropping garbage; 28 TB of compressed overview traffic. Throughput goal:
|
||||
cost: a second implementation of lib/headers, lib/release, lib/rules and
|
||||
the chunk format, kept identical (check.ts) for as long as both exist.
|
||||
Findings, how to run both and how to compare their outputs:
|
||||
`tools/edge-bench-rs/README.md`. Since 2026-10-09 the Rust worker
|
||||
`edge/README.md`. Since 2026-10-09 the Rust worker
|
||||
(`edge-node`, same crate) is the one running on homelab: ~8× less CPU
|
||||
per header and 20× less memory than `edge:node` in production.
|
||||
Per lease: compressed chunks of 10–50 k lines, in order →
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -141,7 +141,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "edge-bench-rs"
|
||||
name = "edge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
@@ -1,8 +1,16 @@
|
||||
[package]
|
||||
name = "edge-bench-rs"
|
||||
name = "edge"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "The edge worker's indexing (lib/edge RangeIndexer) in Rust, to compare with edge:bench on captured ranges"
|
||||
description = "Voyager 2's edge worker (edge-node) and its benchmark (edge-bench): the RangeIndexer in Rust, kept identical to lib/edge (check.ts)"
|
||||
|
||||
[[bin]]
|
||||
name = "edge-bench"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "edge-node"
|
||||
path = "src/bin/edge-node.rs"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
@@ -1,36 +1,40 @@
|
||||
# edge-bench-rs: the edge worker's indexing in Rust
|
||||
# edge: voyager 2's edge worker (Rust)
|
||||
|
||||
Since 2026-10-09 this crate is also the production worker: `edge-node`
|
||||
(`src/bin/edge-node.rs`) leases ranges from the coordinator and posts
|
||||
batches like `node ace edge:node`, with the same indexing as the
|
||||
benchmark (`src/lib.rs`). On homelab it indexed 122M headers in 10
|
||||
minutes with 0.37 cores and 565 MB, next to the TypeScript workers' 24M
|
||||
with 0.61 cores and 11.3 GB (8 threads vs 8 processes): about 8× less
|
||||
CPU per header and 20× less memory. See "The worker" below.
|
||||
The product: `edge-node` (`src/bin/edge-node.rs`) is the worker that runs
|
||||
on homelab (compose service `edge`, image target `edge`). It leases ranges
|
||||
from the node API, reads their prefetched XOVER chunks, finds releases and
|
||||
posts batches. `edge-bench` (`src/main.rs`) replays a captured range through
|
||||
the same indexing (`src/lib.rs`) for timing and for comparing.
|
||||
|
||||
A measurement, not a product. It answers whether a Rust edge worker would be
|
||||
worth a second implementation of the TypeScript one (`edge:node`, REBUILD.md
|
||||
part 2). It ports the path that `edge:bench` replays over a captured range:
|
||||
The TypeScript `RangeIndexer` (`lib/edge`, `node ace edge:node`) is the
|
||||
reference: `check.ts` runs both on a capture and compares every output field
|
||||
by field. An identity or rule change goes into both and has to come out
|
||||
identical. CI (`.gitea/workflows/deploy.yaml`, job `edge`) runs `cargo test`
|
||||
and `check-fixtures.sh`, which compares both on the committed captures in
|
||||
`fixtures/` (25k overview lines each of a.b.models, a.b.ath and
|
||||
a.b.boneless, in 5 chunks); a push to `rebuild` deploys only when they
|
||||
match.
|
||||
|
||||
- inflate each XOVER chunk;
|
||||
- scan the overview lines (no object per line);
|
||||
- apply the owner rule from the Xref;
|
||||
- build segments (file key, release identity);
|
||||
- aggregate segments into files, and cut off idle ones as fragments,
|
||||
per-article or incomplete files;
|
||||
- every N chunks, judge the completed releases (rules: executables,
|
||||
readable names, garbage tools and signatures, per-article random
|
||||
subjects), encode their chunks (format v2) and pick probe candidates.
|
||||
```sh
|
||||
cargo test --release --manifest-path edge/Cargo.toml
|
||||
edge/check-fixtures.sh # from the repository root
|
||||
```
|
||||
|
||||
It does not port NNTP, the coordinator API, batch posting or anything on the
|
||||
server side.
|
||||
In production since 2026-10-09: 122M headers in 10 minutes with 0.37 cores
|
||||
and 565 MB, next to the TypeScript workers' 24M with 0.61 cores and 11.3 GB
|
||||
(8 threads vs 8 processes): about 8× less CPU per header and 20× less
|
||||
memory. See "The worker" below.
|
||||
|
||||
It started as a measurement (moved from `tools/edge-bench-rs` on
|
||||
2026-10-11): whether a Rust worker would be worth a second implementation.
|
||||
The findings below are from then.
|
||||
|
||||
## Findings (2026-10-09)
|
||||
|
||||
Same machine (the workstation, otherwise idle), same captures, 2M headers
|
||||
each:
|
||||
|
||||
| Capture | TypeScript (`edge:bench`) | Rust (`edge-bench-rs`) | Less CPU |
|
||||
| Capture | TypeScript (`edge:bench`) | Rust (`edge-bench`) | Less CPU |
|
||||
|---|---|---|---|
|
||||
| a.b.boneless 2026 (160950000000–160951999999) | 11.1 CPU-s (~215k headers/s), 0.70 GB peak | 2.9 CPU-s (~690k/s), 0.24 GB | 3.8× |
|
||||
| a.b.boneless 2026 (160952000000–160953999999) | 11.8 CPU-s (~205k/s), 0.70 GB | 3.1 CPU-s (~640k/s), 0.24 GB | 3.8× |
|
||||
@@ -92,8 +96,8 @@ Benchmark both on it:
|
||||
|
||||
```sh
|
||||
node ace edge:bench --dir /path/to/capture # TypeScript: headers/s, CPU, inflate/index, peak RSS
|
||||
cd tools/edge-bench-rs && cargo build --release
|
||||
./target/release/edge-bench-rs /path/to/capture # Rust: the same summary
|
||||
cd edge && cargo build --release
|
||||
./target/release/edge-bench /path/to/capture # Rust: the same summary
|
||||
```
|
||||
|
||||
Both take results every 10 chunks (`--batch-chunks`) and give a file up after
|
||||
@@ -109,8 +113,8 @@ themselves.
|
||||
## Comparing the output
|
||||
|
||||
```sh
|
||||
./target/release/edge-bench-rs /path/to/capture --dump /tmp/out.jsonl
|
||||
cd ../.. && node --import=@poppinss/ts-exec tools/edge-bench-rs/check.ts /path/to/capture /tmp/out.jsonl
|
||||
./target/release/edge-bench /path/to/capture --dump /tmp/out.jsonl
|
||||
cd .. && node --import=@poppinss/ts-exec edge/check.ts /path/to/capture /tmp/out.jsonl
|
||||
```
|
||||
|
||||
`--dump` writes every output of every batch as JSON lines:
|
||||
@@ -223,12 +227,12 @@ COORDINATOR_URL=http://coordinator:3334 NODE_TOKEN=… ./target/release/edge-nod
|
||||
node's download cap, shared by its threads. Plus a heartbeat when no
|
||||
batch went out for a minute.
|
||||
- **Wire format:** `src/wire.rs` writes a batch as `toWire` does.
|
||||
`edge-bench-rs --wire` and `check.ts --wire` compare it with the
|
||||
`edge-bench --wire` and `check.ts --wire` compare it with the
|
||||
TypeScript's, field by field.
|
||||
- **End to end:** both workers indexed the a.b.models capture (spooled
|
||||
as a range) through the web server; what the server stored was the
|
||||
same (releases with every column, chunks, candidates, rule counts,
|
||||
fragments with their JSON).
|
||||
- **On homelab:** container `usenet-rebuild-edge-rs` runs
|
||||
`/mnt/bulk/usenet-rebuild/bin/edge-node --threads 8` in
|
||||
node:22-bookworm-slim (the binary needs glibc 2.34+).
|
||||
- **On homelab:** compose service `edge` (deploy/compose.yaml), the
|
||||
`edge` image target of the Dockerfile, `--threads $EDGE_THREADS` (8);
|
||||
it talks to `node-api`.
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# The Rust and TypeScript RangeIndexers on the committed captures (edge/fixtures: 25k
|
||||
# overview lines each of a.b.models, a.b.ath and a.b.boneless, in 5 chunks), compared
|
||||
# by check.ts field by field: exits non-zero on any difference. CI runs it; run it from
|
||||
# the repository root after any change to either side.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
cargo build --release --quiet --manifest-path edge/Cargo.toml --bin edge-bench
|
||||
out=$(mktemp -d)
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
status=0
|
||||
for capture in edge/fixtures/*/; do
|
||||
name=$(basename "$capture")
|
||||
edge/target/release/edge-bench "$capture" --batch-chunks 2 --idle 2 --dump "$out/$name.jsonl" --wire "$out/$name.wire.jsonl" >/dev/null 2>&1
|
||||
echo "== $name"
|
||||
node --import=@poppinss/ts-exec edge/check.ts "$capture" "$out/$name.jsonl" --wire "$out/$name.wire.jsonl" --batch-chunks 2 --idle 2 || status=1
|
||||
done
|
||||
exit $status
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Compares edge-bench-rs's output (--dump) with the TypeScript RangeIndexer
|
||||
* Compares edge-bench's output (--dump) with the TypeScript RangeIndexer
|
||||
* on the same captured range, item by item: releases (every field, and the
|
||||
* payload decoded with lib/chunks), samples, probe candidates, rule counts,
|
||||
* fragments and batch boundaries.
|
||||
*
|
||||
* node --import=@poppinss/ts-exec tools/edge-bench-rs/check.ts <dir> <dump.jsonl> [--wire batches.jsonl] [--batch-chunks 10] [--idle 5]
|
||||
* node --import=@poppinss/ts-exec edge/check.ts <dir> <dump.jsonl> [--wire batches.jsonl] [--batch-chunks 10] [--idle 5]
|
||||
*
|
||||
* With --wire (edge-bench-rs --wire): also each batch as the Rust worker
|
||||
* With --wire (edge-bench --wire): also each batch as the Rust worker
|
||||
* posts it, against toWire of the TypeScript batch, field by field.
|
||||
*/
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"group":"alt.binaries.ath","start":2549992990,"end":2550017989}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"group":"alt.binaries.boneless","start":160950000000,"end":160950024999}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"group":"alt.binaries.models","start":2017215,"end":2042214}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The edge worker in Rust (REBUILD.md, part 2; commands/edge_node.ts): leases
|
||||
//! ranges from the coordinator, reads their XOVER chunks from its stream,
|
||||
//! indexes them (the same RangeIndexer as edge-bench-rs, kept identical to
|
||||
//! indexes them (the same RangeIndexer as edge-bench, kept identical to
|
||||
//! the TypeScript by check.ts) and posts the batches. One range per thread.
|
||||
//!
|
||||
//! COORDINATOR_URL=… NODE_TOKEN=… edge-node [--threads N] [--batch-articles 500000] [--ranges N] [--max-mbit M]
|
||||
@@ -8,9 +8,9 @@
|
||||
//! SIGTERM/SIGINT: each thread gives its range back after the current chunk
|
||||
//! (the next worker resumes at the last stored batch).
|
||||
|
||||
use edge_bench_rs::indexer::{Batch, RangeIndexer};
|
||||
use edge_bench_rs::names::Rules;
|
||||
use edge_bench_rs::wire;
|
||||
use edge::indexer::{Batch, RangeIndexer};
|
||||
use edge::names::Rules;
|
||||
use edge::wire;
|
||||
use flate2::write::GzEncoder;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
|
||||
//! The edge worker's indexing (lib/edge RangeIndexer and what it uses) in
|
||||
//! Rust, kept identical to the TypeScript (check.ts): the edge-bench-rs
|
||||
//! Rust, kept identical to the TypeScript (check.ts): the edge-bench
|
||||
//! benchmark and the edge-node worker share it.
|
||||
|
||||
pub mod codec;
|
||||
@@ -2,14 +2,14 @@
|
||||
//! --capture` (REBUILD.md, part 2: TypeScript or Rust). Same work as
|
||||
//! edge:bench's replay: inflate each chunk, scan it, aggregate files, cut off
|
||||
//! idle ones, and every N chunks judge and encode the releases. `--dump`
|
||||
//! writes every output as JSON lines for tools/edge-bench-rs/check.ts.
|
||||
//! writes every output as JSON lines for edge/check.ts.
|
||||
//!
|
||||
//! edge-bench-rs <dir> [--batch-chunks 10] [--idle 5] [--dump out.jsonl] [--wire batches.jsonl]
|
||||
//! edge-bench <dir> [--batch-chunks 10] [--idle 5] [--dump out.jsonl] [--wire batches.jsonl]
|
||||
|
||||
use base64::Engine;
|
||||
use edge_bench_rs::indexer::{self, Batch, RangeIndexer};
|
||||
use edge_bench_rs::js::json_str;
|
||||
use edge_bench_rs::{names, wire};
|
||||
use edge::indexer::{self, Batch, RangeIndexer};
|
||||
use edge::js::json_str;
|
||||
use edge::{names, wire};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
@@ -150,7 +150,7 @@ fn dump_batch(batch: &Batch, out: &mut impl Write) {
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let dir = args.get(1).expect("usage: edge-bench-rs <dir> [--batch-chunks N] [--idle N] [--dump file]").clone();
|
||||
let dir = args.get(1).expect("usage: edge-bench <dir> [--batch-chunks N] [--idle N] [--dump file]").clone();
|
||||
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
|
||||
let batch_chunks: usize = flag("--batch-chunks").map(|v| v.parse().unwrap()).unwrap_or(10);
|
||||
let idle: u64 = flag("--idle").map(|v| v.parse().unwrap()).unwrap_or(5);
|
||||
@@ -591,6 +591,11 @@ pub fn looks_random(name: &str) -> bool {
|
||||
if (8..=15).contains(&b.len()) && alnum && n_digit > 0 && n_lower > 0 && b[1..].iter().filter(|c| c.is_ascii_uppercase()).count() >= 3 {
|
||||
return true;
|
||||
}
|
||||
// 51294P1CUKR2R35X: capitals and digits only, switching between them all over the token
|
||||
// (COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice).
|
||||
if b.len() >= 12 && b.iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) && b.windows(2).filter(|w| w[0].is_ascii_digit() != w[1].is_ascii_digit()).count() >= 6 {
|
||||
return true;
|
||||
}
|
||||
if !(b.len() >= 16 && alnum) || n_lower == 0 || n_upper == 0 {
|
||||
return false;
|
||||
}
|
||||
@@ -985,6 +990,9 @@ mod tests {
|
||||
fn random_names() {
|
||||
assert!(looks_random("prvjr7hapd0i1ny14pz4"));
|
||||
assert!(looks_random("RmISakytFwXSCEzjVEpaKk"));
|
||||
assert!(looks_random("51294P1CUKR2R35X"));
|
||||
assert!(!looks_random("COD4MW2REMASTERED"));
|
||||
assert!(!looks_random("BEATLES1964REMASTER"));
|
||||
assert!(!looks_random("Some.Show.S01E01.1080p"));
|
||||
assert!(looks_random("VIDEO_TS"));
|
||||
}
|
||||
File renamed without changes.
@@ -5,6 +5,17 @@ import { isGenericName } from './identity.js'
|
||||
* separators, and the other generated shapes seen on Usenet (LOG.md
|
||||
* 2026-10-01 (3)). Kept in step with SQL's is_readable_name().
|
||||
*/
|
||||
/** How often a token goes from a letter to a digit or back. */
|
||||
function letterDigitSwitches(token: string): number {
|
||||
let switches = 0
|
||||
for (let i = 1; i < token.length; i++) {
|
||||
const a = token.charCodeAt(i - 1) <= 57
|
||||
const b = token.charCodeAt(i) <= 57
|
||||
if (a !== b) switches++
|
||||
}
|
||||
return switches
|
||||
}
|
||||
|
||||
export function looksRandom(name: string): boolean {
|
||||
// An unknown short extension (e.g. ".bin") says nothing about the name.
|
||||
name = name.replace(/\.[A-Za-z0-9]{1,4}$/, '')
|
||||
@@ -21,6 +32,9 @@ export function looksRandom(name: string): boolean {
|
||||
if (/^[a-z0-9]{12,}$/.test(name) && (name.match(/\d/g)?.length ?? 0) >= 2 && (name.match(/[a-z]/g)?.length ?? 0) >= 4) return true
|
||||
// wHJ7s71sAGE: short, but capitals all over the token, not only at word starts
|
||||
if (/^[A-Za-z0-9]{8,15}$/.test(name) && /\d/.test(name) && /[a-z]/.test(name) && (name.slice(1).match(/[A-Z]/g)?.length ?? 0) >= 3) return true
|
||||
// 51294P1CUKR2R35X: capitals and digits only, switching between them all over the token
|
||||
// (COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice).
|
||||
if (/^[A-Z0-9]{12,}$/.test(name) && letterDigitSwitches(name) >= 6) return true
|
||||
if (!/^[A-Za-z0-9]{16,}$/.test(name) || !/[a-z]/.test(name) || !/[A-Z]/.test(name)) return false
|
||||
// Digits, or many capitals without separators (e.g. RmISakytFwXSCEzjVEpaKk).
|
||||
return /\d/.test(name) || (name.match(/[A-Z]/g) ?? []).length >= 6
|
||||
|
||||
@@ -78,6 +78,15 @@ test.group('release / releaseIdentity', () => {
|
||||
assert.equal(a.key, b.key)
|
||||
})
|
||||
|
||||
test('capitals and digits switching all over a token are random', async ({ assert }) => {
|
||||
const { looksRandom } = await import('#lib/release/index')
|
||||
assert.isTrue(looksRandom('51294P1CUKR2R35X'))
|
||||
assert.isTrue(looksRandom('51294P1CUKR2R35X.part01.rar'.replace(/\.part01\.rar$/, '')))
|
||||
assert.isFalse(looksRandom('COD4MW2REMASTERED'))
|
||||
assert.isFalse(looksRandom('BEATLES1964REMASTER'))
|
||||
assert.isFalse(looksRandom('HDTV1080P'))
|
||||
})
|
||||
|
||||
test('random extensionless names cannot be grouped by name', ({ assert }) => {
|
||||
const name = '204b914a977e5f891590ff1099c7a8caf1a0be537bfa5bf7f3a8b1213231'
|
||||
assert.isNull(releaseIdentity({ poster: 'x', subject: `"${name}" yEnc`, name, weak: true }))
|
||||
|
||||
Reference in new issue
Block a user