mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-10-21 21:31:21 +00:00
test(ipv6-subnet-calculator): differential suite vs CPython ipaddress; fix /128 netmask
- fix: netmask formula special case was inverted — /128 produced an all-zero mask instead of ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff - add 124-case differential fixture generated with CPython ipaddress (random 128-bit values x prefix set + classic edge cases), comparing expanded/network/first/last/total/netmask against the implementation
This commit is contained in:
parent
cee08bdd54
commit
b2b0af666f
3 changed files with 1286 additions and 3 deletions
|
|
@ -0,0 +1,39 @@
|
|||
// Differential test: the IPv6 calculator must agree with Python's `ipaddress`
|
||||
// stdlib on 124 random + hand-picked cases (fixture generated by CPython).
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describeIpv6 } from './ipv6-subnet-calculator.models';
|
||||
|
||||
interface FixtureCase {
|
||||
input: string
|
||||
expanded: string
|
||||
compressed: string
|
||||
network: string
|
||||
first: string
|
||||
last: string
|
||||
total: string
|
||||
netmask: string
|
||||
}
|
||||
|
||||
const cases: FixtureCase[] = JSON.parse(
|
||||
readFileSync(new URL('./ipv6.fixture.json', import.meta.url), 'utf8'),
|
||||
);
|
||||
|
||||
// Python's ipaddress uses lowercase hex for IPv6 str(); my implementation is
|
||||
// lowercase per RFC 5952 §4.3 recommendation. Normalize for comparison.
|
||||
const lower = (s: string) => s.toLowerCase();
|
||||
|
||||
describe('ipv6-subnet-calculator: differential vs CPython ipaddress', () => {
|
||||
it.each(cases.map(c => [c.input, c]))('case %s', (_input, c) => {
|
||||
const info = describeIpv6(c.input);
|
||||
expect(lower(info.expanded)).toBe(lower(c.expanded));
|
||||
// compression: compare the network VALUE (Python may pick a different
|
||||
// zero-run on ties; the address itself must match) plus the prefix
|
||||
expect(lower(info.network.split('/')[0])).toBe(lower(c.network.split('/')[0]));
|
||||
expect(info.network.endsWith(`/${c.network.split('/')[1]}`)).toBe(true);
|
||||
expect(lower(info.firstAddress)).toBe(lower(c.first));
|
||||
expect(lower(info.lastAddress)).toBe(lower(c.last));
|
||||
expect(info.totalAddresses).toBe(c.total);
|
||||
expect(lower(info.mask)).toBe(lower(c.netmask));
|
||||
});
|
||||
});
|
||||
|
|
@ -135,12 +135,14 @@ export function describeIpv6(cidr: string): Ipv6Info {
|
|||
|
||||
const hostBits = 128 - prefix;
|
||||
const networkValue = prefix === 0 ? 0n : (addressValue >> BigInt(hostBits)) << BigInt(hostBits);
|
||||
const total = hostBits >= 0 ? (1n << BigInt(hostBits)) : 0n;
|
||||
const total = 1n << BigInt(hostBits);
|
||||
const firstValue = networkValue;
|
||||
const lastValue = networkValue + total - 1n;
|
||||
|
||||
const maskHostBits = BigInt(hostBits);
|
||||
const maskValue = hostBits === 0 ? 0n : ((1n << (128n - maskHostBits)) - 1n) << maskHostBits;
|
||||
// Top `prefix` bits set. (2^p - 1) << (128 - p) is exact for every p in
|
||||
// 0..128 — the old special case for hostBits === 0 was inverted and
|
||||
// produced an all-zero netmask for /128 (#differential fixture).
|
||||
const maskValue = ((1n << BigInt(prefix)) - 1n) << BigInt(128 - prefix);
|
||||
|
||||
const { type, scope } = describeType(address);
|
||||
|
||||
|
|
|
|||
1242
src/tools/ipv6-subnet-calculator/ipv6.fixture.json
Normal file
1242
src/tools/ipv6-subnet-calculator/ipv6.fixture.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue