Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174import { describe, it, expect } from 'vitest'import * as fc from 'fast-check'import { parseCube, LutParseError } from './cube'
/** A size-2 cube with every texel value generated, in file point order. */const cubeTextArb = fc.integer({ max: 8, min: 2 }).chain((size) => fc .array(fc.float({ max: 1, min: 0, noDefaultInfinity: true, noNaN: true }), { maxLength: size * size * size * 3, minLength: size * size * size * 3, }) // Normalize -0 so the text round-trip (String(-0) → "0") is exact. .map((values) => ({ size, values: values.map((v) => (v === 0 ? 0 : v)) })),)
/** Serialize a cube the way real-world exporters do: headers, a comment, * blank lines, CRLF endings, and an extra alpha column per data line. */const serializeCube = (size: number, values: readonly number[]): string => [ '# generated by property test', `TITLE "prop"`, `LUT_3D_SIZE ${size}`, 'DOMAIN_MIN 0.0 0.0 0.0', 'DOMAIN_MAX 1.0 1.0 1.0', '', ...Array.from({ length: size * size * size }, (_, i) => { const [r, g, b] = [values[i * 3]!, values[i * 3 + 1]!, values[i * 3 + 2]!] // Float32 values print as their shortest exact repr, so a parse // round-trip is bit-identical. return `${r} ${g} ${b} 1` }), ].join('\r\n')
/** A cube whose point values encode their own index — the file-order pin. */const indexCube = (size: number): string => { const lines = [`LUT_3D_SIZE ${size}`] for (let i = 0; i < size * size * size; i++) { lines.push(`${i} ${i} ${i}`) } return lines.join('\n')}
describe('parseCube', () => { it('round-trips any generated cube through its own serialization', () => { fc.assert( fc.property(cubeTextArb, ({ size, values }) => { const cube = parseCube(serializeCube(size, values)) expect(cube.size).toBe(size) expect(cube.data).toHaveLength(size * size * size * 3) for (let i = 0; i < values.length; i++) { // The extra alpha column never lands in the data, and float32 // values survive the text round-trip bit-exactly. expect(cube.data[i]).toBe(values[i]) } }), ) })
it('preserves the file point order for any cube size', () => { fc.assert( fc.property(fc.integer({ max: 16, min: 2 }), (size) => { const cube = parseCube(indexCube(size)) expect(cube.data).toHaveLength(size * size * size * 3) for (let i = 0; i < size * size * size; i++) { // Point i is texel (r = i % size, g = (i / size) % size, b = i / // size²) in the file's red-fastest order; its value is its index. expect(cube.data[i * 3]).toBe(i) expect(cube.data[i * 3 + 1]).toBe(i) expect(cube.data[i * 3 + 2]).toBe(i) } }), ) })
it('accepts any layout of ignorable lines around the data', () => { fc.assert( fc.property( fc.record({ comment: fc.string(), size: fc.integer({ max: 4, min: 2 }), title: fc.string(), }), ({ size, title, comment }) => { const points = Array.from( { length: size * size * size }, (_, i) => `${i / 10} ${i / 20} ${i / 30}`, ) const text = [ `# ${comment}`, `TITLE "${title}"`, 'DOMAIN_MIN 0.0 0.0 0.0', `LUT_3D_SIZE ${size}`, 'DOMAIN_MAX 1.0 1.0 1.0', '', ...points, '# trailing comment', ].join('\n') const cube = parseCube(text) expect(cube.size).toBe(size) expect(cube.data).toHaveLength(size * size * size * 3) for (let i = 0; i < points.length; i++) { const [r, g, b] = points[i]!.split(' ').map(Number) expect(cube.data[i * 3]).toBeCloseTo(r!, 6) expect(cube.data[i * 3 + 1]).toBeCloseTo(g!, 6) expect(cube.data[i * 3 + 2]).toBeCloseTo(b!, 6) } }, ), ) })
it('fails with LutParseError on any missing size header', () => { fc.assert( fc.property( fc.string({ maxLength: 40 }).filter((s) => !s.includes('LUT_3D_SIZE')), (text) => { expect(() => parseCube(text)).toThrow(LutParseError) }, ), ) })
it('fails with LutParseError on an invalid size header', () => { fc.assert( fc.property( fc.string({ maxLength: 12 }).filter((s) => s.trim() !== ''), (value) => { expect(() => parseCube(`LUT_3D_SIZE ${value}\n0 0 0\n`)).toThrow(LutParseError) }, ), ) })
it('fails with LutParseError when the data point count is wrong', () => { fc.assert( fc.property( fc.record({ size: fc.integer({ max: 4, min: 2 }), // Any point count that is provably not size³ (max 32 points). count: fc.integer({ max: 32, min: 0 }), }), ({ size, count }) => { const text = [ `LUT_3D_SIZE ${size}`, ...Array.from({ length: count }, () => '0.5 0.5 0.5'), ].join('\n') if (count === size * size * size) { return } // not a wrong count expect(() => parseCube(text)).toThrow(/Expected \d+ data points/) }, ), ) })
it('fails with LutParseError on malformed data lines', () => { fc.assert( fc.property( // Too few columns and non-numeric tokens are malformed; a line with // more than three numbers is NOT (extra columns are tolerated). fc .array(fc.oneof(fc.constant('0 0'), fc.constant('x y z')), { maxLength: 6, minLength: 1, }) .map((lines) => `LUT_3D_SIZE 2\n${lines.join('\n')}`), (text) => { expect(() => parseCube(text)).toThrow(LutParseError) }, ), ) })})