Skip to content

Commit 40ed175

Browse files
committed
Add tests comparing to original JS library
1 parent 61f89b2 commit 40ed175

File tree

7 files changed

+301
-8
lines changed

7 files changed

+301
-8
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ jobs:
1919
rebar3-version: "3"
2020
# elixir-version: "1.15.4"
2121
- run: gleam deps download
22-
- run: gleam test
22+
- run: generate_testcases.sh && gleam test
2323
- run: gleam format --check src test

generate_testcases.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
if ! command -v node &> /dev/null
4+
then
5+
exit 1
6+
fi
7+
8+
if ! command -v npm &> /dev/null
9+
then
10+
exit 1
11+
fi
12+
13+
if ! grep -q '"lz-string"' package.json &> /dev/null; then
14+
npm install lz-string --save
15+
else
16+
npm update lz-string
17+
fi
18+
19+
# Run the JS file with Node.js
20+
node lz_string_generate_test_cases.js

gleam.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ links = [{ title = "Gleam", href = "https://gleam.run" }]
1212
[dependencies]
1313
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
1414
gleam_erlang = ">= 0.26.0 and < 1.0.0"
15+
file_streams = ">= 1.1.1 and < 2.0.0"
1516

1617
[dev-dependencies]
1718
gleeunit = ">= 1.0.0 and < 2.0.0"

lz_string_generate_test_cases.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const LZString = require('lz-string');
2+
const fs = require('node:fs');
3+
4+
function writeInt32LE(buffer, value, offset) {
5+
buffer.writeInt32LE(value, offset);
6+
}
7+
8+
function generate_random_string(length){
9+
let string = ""
10+
for (let i = 0; i < length; i++ ){
11+
string += String.fromCodePoint(random_int_in_range())
12+
}
13+
return string
14+
}
15+
16+
function random_int_in_range(){
17+
let j = Math.floor(Math.random() * 65_535)
18+
while (!(j < 55295 || j >= 57344)){
19+
j = Math.floor(Math.random() * 65_535)
20+
}
21+
return j
22+
}
23+
24+
function compressed_sentence(str, type){
25+
if (type == "UINT8") {
26+
return LZString.compressToUint8Array(str)
27+
} else if (type == "Base64"){
28+
return LZString.compressToBase64(str)
29+
} else if (type == "URI"){
30+
return LZString.compressToEncodedURIComponent(str)
31+
}
32+
}
33+
34+
function write_random_strings(amount, type, chars){
35+
for (let i = 0; i <= amount; i++){
36+
let str = generate_random_string(chars)
37+
write_test_case_to_file({
38+
input: str,
39+
output: compressed_sentence(str, type)
40+
})
41+
}
42+
}
43+
function every_UTF8_char(){
44+
let codePoints = [];
45+
46+
// Add code points from 0 to 55295
47+
for (let i = 0; i <= 55295; i++) {
48+
codePoints.push(i);
49+
}
50+
51+
// Add code points from 57344 to 65535
52+
for (let i = 57344; i <= 65535; i++) {
53+
codePoints.push(i);
54+
}
55+
56+
// Convert code points to a string
57+
return String.fromCodePoint(...codePoints);
58+
}
59+
60+
const file = fs.openSync('output.bin', 'w');
61+
62+
63+
function create_cases(){
64+
//2 known cases as a sanity test
65+
write_test_case_to_file({input: "hello, i am a 猫", output: compressed_sentence("hello, i am a 猫", "UINT8")})
66+
write_test_case_to_file({input: "今日は 今日は 今日は 今日は 今日は 今日は", output: compressed_sentence("今日は 今日は 今日は 今日は 今日は 今日は", "UINT8")})
67+
68+
//Generate 1000 random strings and compress them
69+
write_random_strings(1000, "UINT8", 1000)
70+
write_random_strings(1000, "Base64", 1000)
71+
write_random_strings(1000, "URI", 1000)
72+
73+
//Generatea really long string and compress it
74+
write_random_strings(1, "UINT8", 1000000)
75+
write_random_strings(1, "Base64", 1000000)
76+
write_random_strings(1, "URI", 1000000)
77+
78+
let allUTF8Characters = every_UTF8_char()
79+
write_test_case_to_file({input: "", output: compressed_sentence(allUTF8Characters, "UINT8")})
80+
write_test_case_to_file({input: "", output: compressed_sentence(allUTF8Characters, "Base64")})
81+
write_test_case_to_file({input: "", output: compressed_sentence(allUTF8Characters, "URI")})
82+
}
83+
84+
// First 4 bytes are the size of the input string followed by the input string
85+
// and the same for the output string
86+
function write_test_case_to_file(testCase){
87+
const inputBuffer = Buffer.from(testCase.input, 'utf-8');
88+
const outputBuffer = Buffer.from(testCase.output);
89+
90+
const buffer = Buffer.alloc(8 + inputBuffer.length + outputBuffer.length);
91+
writeInt32LE(buffer, inputBuffer.length, 0);
92+
inputBuffer.copy(buffer, 4);
93+
writeInt32LE(buffer, outputBuffer.length, 4 + inputBuffer.length);
94+
outputBuffer.copy(buffer, 8 + inputBuffer.length);
95+
fs.writeSync(file, buffer);
96+
}
97+
98+
create_cases()
99+
fs.closeSync(file);

manifest.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
# You typically do not need to edit this file
33

44
packages = [
5+
{ name = "file_streams", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "file_streams", source = "hex", outer_checksum = "73FC5AD6CA7016E521EFCD7CCB14CA8D186C7C2B0669A53EF7EC18C57A20DBBA" },
56
{ name = "gleam_erlang", version = "0.26.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "3DF72F95F4716883FA51396FB0C550ED3D55195B541568CAF09745984FD37AD1" },
67
{ name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" },
78
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
89
]
910

1011
[requirements]
11-
gleam_erlang = { version = ">= 0.26.0 and < 1.0.0"}
12+
file_streams = { version = ">= 1.1.1 and < 2.0.0"}
13+
gleam_erlang = { version = ">= 0.26.0 and < 1.0.0" }
1214
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
1315
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

src/internal_lib/lib.gleam

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ pub fn decode_base64(
2323
[char, ..rest] -> {
2424
case dict.get(key_dict, char) {
2525
Ok(num) -> {
26-
decode_base64(
27-
rest,
28-
key_dict,
29-
bit_array.append(bitstring, <<num:size(6)>>),
30-
)
26+
decode_base64(rest, key_dict, <<bitstring:bits, <<num:size(6)>>:bits>>)
3127
}
3228
_ -> Error(EInvalidInput)
3329
}

test/gleamlz_string_test.gleam

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
import file_streams/file_stream.{type FileStream}
12
import gleam/bit_array
23
import gleam/erlang/atom
34
import gleam/list
5+
import gleam/result
46
import gleam/string
57
import gleamlz_string
68
import gleeunit
79
import gleeunit/should
810
import helpers/test_helpers
911

12+
type Mode {
13+
Uint8
14+
Base64
15+
URI
16+
}
17+
1018
const known_string = "hello, i am a 猫"
1119

20+
const filename = "output.bin"
21+
1222
pub fn main() {
1323
gleeunit.main()
1424
}
@@ -138,7 +148,7 @@ pub fn high_entropy_string_test_() {
138148

139149
pub fn large_low_entropy_string_test_() {
140150
let assert Ok(timeout) = atom.from_string("timeout")
141-
#(timeout, 60.0, [
151+
#(timeout, 300.0, [
142152
fn() {
143153
let str =
144154
bit_array.base16_encode(test_helpers.generate_random_bytes(1_000_000))
@@ -157,3 +167,168 @@ pub fn large_low_entropy_string_test_() {
157167
},
158168
])
159169
}
170+
171+
pub fn invalid_input_test() {
172+
gleamlz_string.decompress_from_uint8(<<5>>)
173+
|> should.be_error
174+
175+
gleamlz_string.decompress_from_base64(known_string)
176+
|> should.be_error
177+
178+
gleamlz_string.decompress_from_encoded_uri(known_string)
179+
|> should.be_error
180+
}
181+
182+
//Tests with the OG javascript library output
183+
184+
pub fn js_lib_test_() {
185+
let assert Ok(timeout) = atom.from_string("timeout")
186+
#(timeout, 300.0, [
187+
fn() {
188+
let assert Ok(stream) = file_stream.open_read(filename)
189+
//Test 2 known strings
190+
js_test_known(stream)
191+
|> should.be_ok
192+
193+
//1000 strings of 1_000 chars each
194+
js_test_random_uint8(stream, 1000)
195+
|> should.be_ok
196+
197+
js_test_random_base64(stream, 1000)
198+
|> should.be_ok
199+
200+
js_test_random_uri(stream, 1000)
201+
|> should.be_ok
202+
203+
//1 string 1_000_000 chars
204+
js_test_random_uint8(stream, 1)
205+
|> should.be_ok
206+
207+
js_test_random_base64(stream, 1)
208+
|> should.be_ok
209+
210+
js_test_random_uri(stream, 1)
211+
|> should.be_ok
212+
213+
//every single utf8 character
214+
js_test_all_utf8_uint8(stream)
215+
|> should.be_ok
216+
217+
js_test_all_utf8_base64(stream)
218+
|> should.be_ok
219+
|> should.be_ok
220+
221+
js_test_all_utf8_uri(stream)
222+
|> should.be_ok
223+
|> should.be_ok
224+
225+
let assert Ok(Nil) = file_stream.close(stream)
226+
},
227+
])
228+
}
229+
230+
fn js_test_known(stream: FileStream) {
231+
[
232+
"hello, i am a 猫",
233+
"今日は 今日は 今日は 今日は 今日は 今日は",
234+
]
235+
|> list.try_each(fn(string) { js_compress_vs_known(stream, string) })
236+
}
237+
238+
fn js_test_random_uint8(fstream: FileStream, n: Int) {
239+
list.range(0, n)
240+
|> list.try_each(fn(_x) { js_decompress_vs_unknown(fstream, Uint8) })
241+
}
242+
243+
fn js_test_random_base64(fstream: FileStream, n: Int) {
244+
list.range(0, n)
245+
|> list.try_each(fn(_x) { js_decompress_vs_unknown(fstream, Base64) })
246+
}
247+
248+
pub fn js_test_random_uri(fstream: FileStream, n: Int) {
249+
list.range(0, n)
250+
|> list.try_each(fn(_x) { js_decompress_vs_unknown(fstream, URI) })
251+
}
252+
253+
fn js_test_all_utf8_uint8(fstream: FileStream) {
254+
let allutf8chars = test_helpers.all_utf8_chars()
255+
256+
use #(_input_str, output_str) <- result.map(read_js_input_output(fstream))
257+
258+
output_str
259+
|> gleamlz_string.decompress_from_uint8()
260+
|> should.equal(Ok(allutf8chars))
261+
}
262+
263+
fn js_test_all_utf8_base64(fstream: FileStream) {
264+
let allutf8chars = test_helpers.all_utf8_chars()
265+
js_decompress_vs_known(fstream, allutf8chars, Base64)
266+
}
267+
268+
fn js_test_all_utf8_uri(fstream: FileStream) {
269+
let allutf8chars = test_helpers.all_utf8_chars()
270+
js_decompress_vs_known(fstream, allutf8chars, URI)
271+
}
272+
273+
//Compress a known string and match the JS output for the same
274+
fn js_compress_vs_known(fstream: FileStream, known_string: String) {
275+
result.map(read_js_input_output(fstream), fn(result) {
276+
let compressed = gleamlz_string.compress_to_uint8(known_string)
277+
compressed
278+
|> should.equal(result.1)
279+
280+
compressed
281+
|> should.not_equal(<<>>)
282+
})
283+
}
284+
285+
fn js_decompress_vs_known(fstream: FileStream, known_string: String, mode: Mode) {
286+
use result <- result.map(read_js_input_output(fstream))
287+
use output_str <- result.map(bit_array.to_string(result.1))
288+
case mode {
289+
Uint8 -> {
290+
gleamlz_string.decompress_from_uint8(result.1)
291+
|> should.equal(Ok(known_string))
292+
}
293+
Base64 -> {
294+
gleamlz_string.decompress_from_base64(output_str)
295+
|> should.equal(Ok(known_string))
296+
}
297+
URI -> {
298+
gleamlz_string.decompress_from_encoded_uri(output_str)
299+
|> should.equal(Ok(known_string))
300+
}
301+
}
302+
}
303+
304+
fn js_decompress_vs_unknown(fstream: FileStream, mode: Mode) {
305+
use result <- result.map(read_js_input_output(fstream))
306+
use input_str <- result.map(bit_array.to_string(result.0))
307+
use output_str <- result.map(bit_array.to_string(result.1))
308+
309+
case mode {
310+
Uint8 -> {
311+
gleamlz_string.decompress_from_uint8(result.1)
312+
|> should.equal(Ok(input_str))
313+
}
314+
Base64 -> {
315+
gleamlz_string.decompress_from_base64(output_str)
316+
|> should.equal(Ok(input_str))
317+
}
318+
URI -> {
319+
gleamlz_string.decompress_from_encoded_uri(output_str)
320+
|> should.equal(Ok(input_str))
321+
}
322+
}
323+
}
324+
325+
fn read_js_input_output(stream: FileStream) {
326+
use input_size <- result.try(file_stream.read_uint32_le(stream))
327+
use str <- result.try(file_stream.read_bytes(stream, input_size))
328+
use output_size <- result.try(file_stream.read_uint32_le(stream))
329+
use js_compressed_string <- result.map(file_stream.read_bytes(
330+
stream,
331+
output_size,
332+
))
333+
#(str, js_compressed_string)
334+
}

0 commit comments

Comments
 (0)