From 93ee4c8a38a808b7b5380644c44c867c90d601fe Mon Sep 17 00:00:00 2001 From: Marc Redemske Date: Tue, 19 Dec 2023 22:55:12 +0100 Subject: [PATCH] chore: move code to new home --- .bazelignore | 1 + .bazelrc | 1 + .bazelversion | 1 + .github/workflows/ci.bazelrc | 5 + .github/workflows/ci.yaml | 40 +++ .gitignore | 1 + BUILD.bazel | 26 ++ MODULE.bazel | 14 + WORKSPACE | 0 defs.bzl | 7 + e2e/.bazelrc | 1 + e2e/BUILD.bazel | 19 ++ e2e/MODULE.bazel | 12 + e2e/README.md | 5 + lib/BUILD.bazel | 20 ++ lib/constants.bzl | 566 +++++++++++++++++++++++++++++++++++ lib/sqids.bzl | 291 ++++++++++++++++++ lib/tests/BUILD.bazel | 12 + lib/tests/alphabet.bzl | 127 ++++++++ lib/tests/blocklist.bzl | 149 +++++++++ lib/tests/encoding.bzl | 187 ++++++++++++ lib/tests/min_length.bzl | 156 ++++++++++ lib/tests/utils.bzl | 25 ++ 23 files changed, 1666 insertions(+) create mode 100644 .bazelignore create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .github/workflows/ci.bazelrc create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 BUILD.bazel create mode 100644 MODULE.bazel create mode 100644 WORKSPACE create mode 100644 defs.bzl create mode 100644 e2e/.bazelrc create mode 100644 e2e/BUILD.bazel create mode 100644 e2e/MODULE.bazel create mode 100644 e2e/README.md create mode 100644 lib/BUILD.bazel create mode 100644 lib/constants.bzl create mode 100644 lib/sqids.bzl create mode 100644 lib/tests/BUILD.bazel create mode 100644 lib/tests/alphabet.bzl create mode 100644 lib/tests/blocklist.bzl create mode 100644 lib/tests/encoding.bzl create mode 100644 lib/tests/min_length.bzl create mode 100644 lib/tests/utils.bzl diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..b1e3af6 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +e2e/ diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..3ce91d2 --- /dev/null +++ b/.bazelrc @@ -0,0 +1 @@ +common --enable_bzlmod diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..19b860c --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +6.4.0 diff --git a/.github/workflows/ci.bazelrc b/.github/workflows/ci.bazelrc new file mode 100644 index 0000000..22ee967 --- /dev/null +++ b/.github/workflows/ci.bazelrc @@ -0,0 +1,5 @@ +# Used in github workflow `ci.yaml` +build --disk_cache=~/.cache/bazel +build --repository_cache=~/.cache/bazel-repo +test --test_output=errors +test --test_env=XDG_CACHE_HOME diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d939b74 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + env: + XDG_CACHE_HOME: ~/.cache/bazel-repo + strategy: + matrix: + bazelversion: + - 6.4.0 + - 7.0.0 + folder: + - "." + - "e2e" + steps: + - uses: actions/checkout@v4 + - name: Mount bazel caches + uses: actions/cache@v3 + with: + path: | + ~/.cache/bazel + ~/.cache/bazel-repo + key: bazel-cache-${{ hashFiles('**/BUILD.bazel', '**/*.bzl', 'WORKSPACE') }} + restore-keys: bazel-cache- + - name: Tests + working-directory: ${{ matrix.folder }} + run: > + USE_BAZEL_VERSION="${{ matrix.bazelversion }}" + bazel + --bazelrc=${{ github.workspace }}/.bazelrc + --bazelrc=${{ github.workspace }}/.github/workflows/ci.bazelrc + test //... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac51a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bazel-* diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..c905410 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,26 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@buildifier_prebuilt//:rules.bzl", "buildifier", "buildifier_test") + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + visibility = ["//visibility:public"], + deps = ["//lib:sqids"], +) + +buildifier( + name = "format", + exclude_patterns = [ + "./.git/*", + ], + lint_mode = "fix", + mode = "fix", +) + +buildifier_test( + name = "buildifier_test", + lint_mode = "warn", + mode = "check", + no_sandbox = True, + workspace = "//:WORKSPACE", +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..e36dd72 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,14 @@ +module( + name = "sqids_bazel", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep( + name = "buildifier_prebuilt", + version = "6.4.0", + dev_dependency = True, +) + +bazel_dep(name = "bazel_skylib", version = "1.5.0") +bazel_dep(name = "aspect_bazel_lib", version = "1.39.0") diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..e69de29 diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 0000000..746b488 --- /dev/null +++ b/defs.bzl @@ -0,0 +1,7 @@ +"Public API" + +load("//lib:sqids.bzl", _decode = "decode", _encode = "encode", _sqids = "sqids") + +decode = _decode +encode = _encode +sqids = _sqids diff --git a/e2e/.bazelrc b/e2e/.bazelrc new file mode 100644 index 0000000..3ce91d2 --- /dev/null +++ b/e2e/.bazelrc @@ -0,0 +1 @@ +common --enable_bzlmod diff --git a/e2e/BUILD.bazel b/e2e/BUILD.bazel new file mode 100644 index 0000000..339563f --- /dev/null +++ b/e2e/BUILD.bazel @@ -0,0 +1,19 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@sqids_bazel//:defs.bzl", "sqids") + +hash = sqids().encode([ + 1, + 2, + 3, +]) + +genrule( + name = "file", + outs = ["test-%s" % hash], + cmd = "echo '%s' > $(OUTS)" % hash, +) + +build_test( + name = "test", + targets = [":file"], +) diff --git a/e2e/MODULE.bazel b/e2e/MODULE.bazel new file mode 100644 index 0000000..2db4766 --- /dev/null +++ b/e2e/MODULE.bazel @@ -0,0 +1,12 @@ +module( + name = "sqids_bazel_e2e", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.5.0") +bazel_dep(name = "sqids_bazel", version = "0.0.0") +local_path_override( + module_name = "sqids_bazel", + path = "..", +) diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..b01a45a --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,5 @@ +# e2e + +```bash +cd e2e; bazel test //... +``` diff --git a/lib/BUILD.bazel b/lib/BUILD.bazel new file mode 100644 index 0000000..0f6dd4c --- /dev/null +++ b/lib/BUILD.bazel @@ -0,0 +1,20 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "constants", + srcs = ["constants.bzl"], + visibility = [ + "//lib:__subpackages__", + "//lib/tests:__subpackages__", + ], +) + +bzl_library( + name = "sqids", + srcs = ["sqids.bzl"], + visibility = ["//:__pkg__"], + deps = [ + ":constants", + "@aspect_bazel_lib//lib:strings", + ], +) diff --git a/lib/constants.bzl b/lib/constants.bzl new file mode 100644 index 0000000..da3bc86 --- /dev/null +++ b/lib/constants.bzl @@ -0,0 +1,566 @@ +"Constants" + +DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +DEFAULT_MIN_LENGTH = 0 +DEFAULT_BLOCKLIST = [ + "0rgasm", + "1d10t", + "1d1ot", + "1di0t", + "1diot", + "1eccacu10", + "1eccacu1o", + "1eccacul0", + "1eccaculo", + "1mbec11e", + "1mbec1le", + "1mbeci1e", + "1mbecile", + "a11upat0", + "a11upato", + "a1lupat0", + "a1lupato", + "aand", + "ah01e", + "ah0le", + "aho1e", + "ahole", + "al1upat0", + "al1upato", + "allupat0", + "allupato", + "ana1", + "ana1e", + "anal", + "anale", + "anus", + "arrapat0", + "arrapato", + "arsch", + "arse", + "ass", + "b00b", + "b00be", + "b01ata", + "b0ceta", + "b0iata", + "b0ob", + "b0obe", + "b0sta", + "b1tch", + "b1te", + "b1tte", + "ba1atkar", + "balatkar", + "bastard0", + "bastardo", + "batt0na", + "battona", + "bitch", + "bite", + "bitte", + "bo0b", + "bo0be", + "bo1ata", + "boceta", + "boiata", + "boob", + "boobe", + "bosta", + "bran1age", + "bran1er", + "bran1ette", + "bran1eur", + "bran1euse", + "branlage", + "branler", + "branlette", + "branleur", + "branleuse", + "c0ck", + "c0g110ne", + "c0g11one", + "c0g1i0ne", + "c0g1ione", + "c0gl10ne", + "c0gl1one", + "c0gli0ne", + "c0glione", + "c0na", + "c0nnard", + "c0nnasse", + "c0nne", + "c0u111es", + "c0u11les", + "c0u1l1es", + "c0u1lles", + "c0ui11es", + "c0ui1les", + "c0uil1es", + "c0uilles", + "c11t", + "c11t0", + "c11to", + "c1it", + "c1it0", + "c1ito", + "cabr0n", + "cabra0", + "cabrao", + "cabron", + "caca", + "cacca", + "cacete", + "cagante", + "cagar", + "cagare", + "cagna", + "cara1h0", + "cara1ho", + "caracu10", + "caracu1o", + "caracul0", + "caraculo", + "caralh0", + "caralho", + "cazz0", + "cazz1mma", + "cazzata", + "cazzimma", + "cazzo", + "ch00t1a", + "ch00t1ya", + "ch00tia", + "ch00tiya", + "ch0d", + "ch0ot1a", + "ch0ot1ya", + "ch0otia", + "ch0otiya", + "ch1asse", + "ch1avata", + "ch1er", + "ch1ng0", + "ch1ngadaz0s", + "ch1ngadazos", + "ch1ngader1ta", + "ch1ngaderita", + "ch1ngar", + "ch1ngo", + "ch1ngues", + "ch1nk", + "chatte", + "chiasse", + "chiavata", + "chier", + "ching0", + "chingadaz0s", + "chingadazos", + "chingader1ta", + "chingaderita", + "chingar", + "chingo", + "chingues", + "chink", + "cho0t1a", + "cho0t1ya", + "cho0tia", + "cho0tiya", + "chod", + "choot1a", + "choot1ya", + "chootia", + "chootiya", + "cl1t", + "cl1t0", + "cl1to", + "clit", + "clit0", + "clito", + "cock", + "cog110ne", + "cog11one", + "cog1i0ne", + "cog1ione", + "cogl10ne", + "cogl1one", + "cogli0ne", + "coglione", + "cona", + "connard", + "connasse", + "conne", + "cou111es", + "cou11les", + "cou1l1es", + "cou1lles", + "coui11es", + "coui1les", + "couil1es", + "couilles", + "cracker", + "crap", + "cu10", + "cu1att0ne", + "cu1attone", + "cu1er0", + "cu1ero", + "cu1o", + "cul0", + "culatt0ne", + "culattone", + "culer0", + "culero", + "culo", + "cum", + "cunt", + "d11d0", + "d11do", + "d1ck", + "d1ld0", + "d1ldo", + "damn", + "de1ch", + "deich", + "depp", + "di1d0", + "di1do", + "dick", + "dild0", + "dildo", + "dyke", + "encu1e", + "encule", + "enema", + "enf01re", + "enf0ire", + "enfo1re", + "enfoire", + "estup1d0", + "estup1do", + "estupid0", + "estupido", + "etr0n", + "etron", + "f0da", + "f0der", + "f0ttere", + "f0tters1", + "f0ttersi", + "f0tze", + "f0utre", + "f1ca", + "f1cker", + "f1ga", + "fag", + "fica", + "ficker", + "figa", + "foda", + "foder", + "fottere", + "fotters1", + "fottersi", + "fotze", + "foutre", + "fr0c10", + "fr0c1o", + "fr0ci0", + "fr0cio", + "fr0sc10", + "fr0sc1o", + "fr0sci0", + "fr0scio", + "froc10", + "froc1o", + "froci0", + "frocio", + "frosc10", + "frosc1o", + "frosci0", + "froscio", + "fuck", + "g00", + "g0o", + "g0u1ne", + "g0uine", + "gandu", + "go0", + "goo", + "gou1ne", + "gouine", + "gr0gnasse", + "grognasse", + "haram1", + "harami", + "haramzade", + "hund1n", + "hundin", + "id10t", + "id1ot", + "idi0t", + "idiot", + "imbec11e", + "imbec1le", + "imbeci1e", + "imbecile", + "j1zz", + "jerk", + "jizz", + "k1ke", + "kam1ne", + "kamine", + "kike", + "leccacu10", + "leccacu1o", + "leccacul0", + "leccaculo", + "m1erda", + "m1gn0tta", + "m1gnotta", + "m1nch1a", + "m1nchia", + "m1st", + "mam0n", + "mamahuev0", + "mamahuevo", + "mamon", + "masturbat10n", + "masturbat1on", + "masturbate", + "masturbati0n", + "masturbation", + "merd0s0", + "merd0so", + "merda", + "merde", + "merdos0", + "merdoso", + "mierda", + "mign0tta", + "mignotta", + "minch1a", + "minchia", + "mist", + "musch1", + "muschi", + "n1gger", + "neger", + "negr0", + "negre", + "negro", + "nerch1a", + "nerchia", + "nigger", + "orgasm", + "p00p", + "p011a", + "p01la", + "p0l1a", + "p0lla", + "p0mp1n0", + "p0mp1no", + "p0mpin0", + "p0mpino", + "p0op", + "p0rca", + "p0rn", + "p0rra", + "p0uff1asse", + "p0uffiasse", + "p1p1", + "p1pi", + "p1r1a", + "p1rla", + "p1sc10", + "p1sc1o", + "p1sci0", + "p1scio", + "p1sser", + "pa11e", + "pa1le", + "pal1e", + "palle", + "pane1e1r0", + "pane1e1ro", + "pane1eir0", + "pane1eiro", + "panele1r0", + "panele1ro", + "paneleir0", + "paneleiro", + "patakha", + "pec0r1na", + "pec0rina", + "pecor1na", + "pecorina", + "pen1s", + "pendej0", + "pendejo", + "penis", + "pip1", + "pipi", + "pir1a", + "pirla", + "pisc10", + "pisc1o", + "pisci0", + "piscio", + "pisser", + "po0p", + "po11a", + "po1la", + "pol1a", + "polla", + "pomp1n0", + "pomp1no", + "pompin0", + "pompino", + "poop", + "porca", + "porn", + "porra", + "pouff1asse", + "pouffiasse", + "pr1ck", + "prick", + "pussy", + "put1za", + "puta", + "puta1n", + "putain", + "pute", + "putiza", + "puttana", + "queca", + "r0mp1ba11e", + "r0mp1ba1le", + "r0mp1bal1e", + "r0mp1balle", + "r0mpiba11e", + "r0mpiba1le", + "r0mpibal1e", + "r0mpiballe", + "rand1", + "randi", + "rape", + "recch10ne", + "recch1one", + "recchi0ne", + "recchione", + "retard", + "romp1ba11e", + "romp1ba1le", + "romp1bal1e", + "romp1balle", + "rompiba11e", + "rompiba1le", + "rompibal1e", + "rompiballe", + "ruff1an0", + "ruff1ano", + "ruffian0", + "ruffiano", + "s1ut", + "sa10pe", + "sa1aud", + "sa1ope", + "sacanagem", + "sal0pe", + "salaud", + "salope", + "saugnapf", + "sb0rr0ne", + "sb0rra", + "sb0rrone", + "sbattere", + "sbatters1", + "sbattersi", + "sborr0ne", + "sborra", + "sborrone", + "sc0pare", + "sc0pata", + "sch1ampe", + "sche1se", + "sche1sse", + "scheise", + "scheisse", + "schlampe", + "schwachs1nn1g", + "schwachs1nnig", + "schwachsinn1g", + "schwachsinnig", + "schwanz", + "scopare", + "scopata", + "sexy", + "sh1t", + "shit", + "slut", + "sp0mp1nare", + "sp0mpinare", + "spomp1nare", + "spompinare", + "str0nz0", + "str0nza", + "str0nzo", + "stronz0", + "stronza", + "stronzo", + "stup1d", + "stupid", + "succh1am1", + "succh1ami", + "succhiam1", + "succhiami", + "sucker", + "t0pa", + "tapette", + "test1c1e", + "test1cle", + "testic1e", + "testicle", + "tette", + "topa", + "tr01a", + "tr0ia", + "tr0mbare", + "tr1ng1er", + "tr1ngler", + "tring1er", + "tringler", + "tro1a", + "troia", + "trombare", + "turd", + "twat", + "vaffancu10", + "vaffancu1o", + "vaffancul0", + "vaffanculo", + "vag1na", + "vagina", + "verdammt", + "verga", + "w1chsen", + "wank", + "wichsen", + "x0ch0ta", + "x0chota", + "xana", + "xoch0ta", + "xochota", + "z0cc01a", + "z0cc0la", + "z0cco1a", + "z0ccola", + "z1z1", + "z1zi", + "ziz1", + "zizi", + "zocc01a", + "zocc0la", + "zocco1a", + "zoccola", +] diff --git a/lib/sqids.bzl b/lib/sqids.bzl new file mode 100644 index 0000000..24d37b1 --- /dev/null +++ b/lib/sqids.bzl @@ -0,0 +1,291 @@ +"""Sqids Bazel + +Generate unique IDs from numbers + +This module implements encoding and decoding [sqids](https://sqids.org/). +The code is heavily adopted from [sqids-python](https://github.com/sqids/sqids-python). +""" + +load("@aspect_bazel_lib//lib:strings.bzl", "ord") +load("//lib:constants.bzl", "DEFAULT_ALPHABET", "DEFAULT_BLOCKLIST", "DEFAULT_MIN_LENGTH") + +_FOREVER = range(1073741824) + +def _unique(arr): + ret = list() + for a in arr: + if a not in ret: + ret.append(a) + return ret + +def _sum(numbers): + ret = 0 + for n in numbers: + ret += n + return ret + +def _to_id(num, alphabet): + id_chars = [] + chars = alphabet.elems() + result = num + + for _ in _FOREVER: + id_chars.insert(0, chars[result % len(chars)]) + result = result // len(chars) + if result == 0: + break + + return "".join(id_chars) + +def _shuffle(alphabet): + chars = list(alphabet.elems()) + + j = len(chars) - 1 + for i in _FOREVER: + if j > 0: + r = (i * j + ord(chars[i]) + ord(chars[j])) % len(chars) + chars[i], chars[r] = chars[r], chars[i] + j -= 1 + continue + break + + return "".join(chars) + +def _is_blocked_id(id_, blocklist): + id_ = id_.lower() + + for word in blocklist: + if len(word) > len(id_): + continue + if len(id_) <= 3 or len(word) <= 3: + if id_ == word: + return True + elif any([c.isdigit() for c in word.elems()]): + if id_.startswith(word) or id_.endswith(word): + return True + elif word in id_: + return True + + return False + +def _filter_blocklist(blocklist, alphabet): + filtered_blocklist = list() + alphabet_lower = alphabet.lower() + for word_lower in [w.lower() for w in blocklist if len(w) >= 3]: + intersection = [c for c in word_lower.elems() if c in alphabet_lower] + if len(intersection) == len(word_lower): + filtered_blocklist.append(word_lower) + return _unique(filtered_blocklist) + +def _encode_numbers(options, numbers, increment): + if increment > len(options.alphabet): + fail("Reached max attempts to re-generate the ID") + + offset = _sum( + [ + ord(options.alphabet[v % len(options.alphabet)]) + i + for i, v in enumerate(numbers) + ], + ) + offset = (offset + len(numbers)) % len(options.alphabet) + offset = (offset + increment) % len(options.alphabet) + alphabet = options.alphabet[offset:] + options.alphabet[:offset] + prefix = alphabet[0] + alphabet = alphabet[::-1] + ret = [prefix] + + for i, num in enumerate(numbers): + ret.append(_to_id(num, alphabet[1:])) + + if i >= len(numbers) - 1: + continue + + ret.append(alphabet[0]) + alphabet = _shuffle(alphabet) + + id_ = "".join(ret) + + if options.min_length > len(id_): + id_ += alphabet[0] + + for _ in _FOREVER: + if (options.min_length - len(id_)) > 0: + alphabet = _shuffle(alphabet) + id_ += alphabet[:min(options.min_length - len(id_), len(alphabet))] + continue + break + + return id_ + +def _sqrt(a, b): + if b == 0: + return 1 + ret = a + for _ in range(b - 1): + ret *= a + return ret + +def _to_number(alphabet, id_): + chars = alphabet.elems() + return _sum([chars.index(c) * _sqrt(len(chars), i) for i, c in enumerate(id_[::-1].elems())]) + +def _decode(options, id_): + ret = [] + + if not id_: + return ret + + alphabet_chars = options.alphabet.elems() + if any([c not in alphabet_chars for c in id_.elems()]): + return ret + + prefix = id_[0] + offset = options.alphabet.index(prefix) + alphabet = options.alphabet[offset:] + options.alphabet[:offset] + alphabet = alphabet[::-1] + id_ = id_[1:] + + for _ in _FOREVER: + if id_: + separator = alphabet[0] + chunks = id_.split(separator) + if chunks: + if not chunks[0]: + return ret + + ret.append(_to_number(alphabet[1:], chunks[0])) + if len(chunks) > 1: + alphabet = _shuffle(alphabet) + id_ = separator.join(chunks[1:]) + continue + break + return ret + +def _encode(options, numbers): + id = "" + if numbers == None: + return id + + in_range_numbers = [n for n in numbers if n >= 0] + if len(in_range_numbers) != len(numbers): + fail("Encoding supports numbers greater than 0") + + for i in _FOREVER: + id = _encode_numbers(options, numbers, i) + if not _is_blocked_id(id, options.blocklist): + break + return id + +def _check_options(alphabet, min_length): + for char in alphabet.elems(): + if ord(char) > 127: + fail("Alphabet cannot contain multibyte characters") + + if len(alphabet) < 3: + fail("Alphabet length must be at least 3") + + if len(_unique(alphabet.elems())) != len(alphabet): + fail("Alphabet must contain unique characters") + + if type(min_length) != "int": + fail("Minimum length must be an integer") + + MIN_LENGTH_LIMIT = 255 + if min_length < 0 or min_length > MIN_LENGTH_LIMIT: + fail("Minimum length has to be between 0 and %s" % MIN_LENGTH_LIMIT) + +def encode(numbers = None, alphabet = DEFAULT_ALPHABET, blocklist = DEFAULT_BLOCKLIST, min_length = DEFAULT_MIN_LENGTH): + """encode a list of numbers to an id + + Example: + ```starlark + load("@sqids_bazel//:defs.bzl", "encode") + + print(encode([1, 2, 3])) // 86Rf07 + ``` + + Args: + numbers: list of numbers to encode + alphabet: list of characters to generate the ids from + blocklist: list of words to avoid in hashes + min_length: minimal count of characters + + Returns: + A generated id as string + """ + _check_options(alphabet, min_length) + options = struct( + alphabet = _shuffle(alphabet), + blocklist = _filter_blocklist(blocklist, alphabet), + min_length = min_length, + ) + return _encode(options, numbers) + +def decode(id, alphabet = DEFAULT_ALPHABET): + """decode a string to a list of numbers + + Example: + ```starlark + load("@sqids_bazel//:defs.bzl", "decode") + + print(decode("86Rf07")) // [1, 2, 3] + ``` + + Args: + id: list of numbers to encode + alphabet: list of characters to generate the ids from + + Returns: + A list of numbers + """ + _check_options(alphabet, DEFAULT_MIN_LENGTH) + options = struct( + alphabet = _shuffle(alphabet), + ) + return _decode(options, id) + +def sqids(alphabet = DEFAULT_ALPHABET, blocklist = DEFAULT_BLOCKLIST, min_length = DEFAULT_MIN_LENGTH): + """Generate unique IDs from numbers + + For convinience, export a function that takes optional arguments alphabet, + blocklist and min_length and exports a `struct` that holds `decode` and + `encode` methods which only take an id or a list of numbers, respectively. + + This API is similar to the [javascript](https://github.com/sqids/sqids-javascript) or + [python](https://github.com/sqids/sqids-python) implementation. + + Example: + ```starlark + load("@sqids_bazel//:defs.bzl", "sqids") + + s = sqids() + print(s.encode([1, 2, 3])) // 86Rf07 + ``` + + Args: + alphabet: list of characters to generate the ids from + blocklist: list of words to avoid in hashes + min_length: minimal count of characters + + Returns: + A `struct` holding the `encode` and `decode` methods. + """ + + _check_options(alphabet, min_length) + + options = struct( + alphabet = _shuffle(alphabet), + blocklist = _filter_blocklist(blocklist, alphabet), + min_length = min_length, + ) + + def encode_wrapper(numbers = None): + return _encode(options, numbers) + + def decode_wrapper(id = None): + return _decode(options, id) + + return struct( + encode = encode_wrapper, + decode = decode_wrapper, + ) diff --git a/lib/tests/BUILD.bazel b/lib/tests/BUILD.bazel new file mode 100644 index 0000000..d0c7ef5 --- /dev/null +++ b/lib/tests/BUILD.bazel @@ -0,0 +1,12 @@ +load(":alphabet.bzl", "alphabet_test_suite") +load(":blocklist.bzl", "blocklist_test_suite") +load(":encoding.bzl", "encoding_test_suite") +load(":min_length.bzl", "min_length_test_suite") + +alphabet_test_suite() + +blocklist_test_suite() + +encoding_test_suite() + +min_length_test_suite() diff --git a/lib/tests/alphabet.bzl b/lib/tests/alphabet.bzl new file mode 100644 index 0000000..657c0c3 --- /dev/null +++ b/lib/tests/alphabet.bzl @@ -0,0 +1,127 @@ +"Alphabet Tests" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("//:defs.bzl", "decode", "encode", "sqids") +load("//lib/tests:utils.bzl", "hashed_file") + +def _simple_test_impl(ctx): + env = unittest.begin(ctx) + + alphabet = "0123456789abcdef" + s = sqids(alphabet = alphabet) + + numbers = [1, 2, 3] + id = "489158" + + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, alphabet), id) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id, alphabet), numbers) + + return unittest.end(env) + +simple_test = unittest.make(_simple_test_impl) + +def _short_alphabet_test_impl(ctx): + env = unittest.begin(ctx) + + alphabet = "abc" + s = sqids(alphabet = alphabet) + + numbers = [1, 2, 3] + + asserts.equals(env, s.decode(s.encode(numbers)), numbers) + asserts.equals(env, decode(encode(numbers, alphabet), alphabet), numbers) + + return unittest.end(env) + +short_alphabet_test = unittest.make(_short_alphabet_test_impl) + +def _long_alphabet_test_impl(ctx): + env = unittest.begin(ctx) + + alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:'\"/?.>,<`~" + s = sqids(alphabet = alphabet) + + numbers = [1, 2, 3] + + asserts.equals(env, s.decode(s.encode(numbers)), numbers) + asserts.equals(env, decode(encode(numbers, alphabet), alphabet), numbers) + + return unittest.end(env) + +long_alphabet_test = unittest.make(_long_alphabet_test_impl) + +def _multibyte_alphabet_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Alphabet cannot contain multibyte characters") + + return analysistest.end(env) + +multibyte_alphabet_test = analysistest.make(_multibyte_alphabet_test_impl, expect_failure = True) + +def _repeating_alphabet_characters_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Alphabet must contain unique characters") + + return analysistest.end(env) + +repeating_alphabet_characters_test = analysistest.make( + _repeating_alphabet_characters_test_impl, + expect_failure = True, +) + +def _too_short_alphabet_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Alphabet length must be at least 3") + + return analysistest.end(env) + +too_short_alphabet_test = analysistest.make(_too_short_alphabet_test_impl, expect_failure = True) + +def alphabet_test_suite(name = "alphabet_test_suite"): + """Alphabet Test Suite + + Args: + name: Name of the testsuite + """ + + multibyte_alphabet_test( + name = "multibyte_alphabet_test", + target_under_test = ":multibyte_alphabet_test_fake_target", + ) + hashed_file( + name = "multibyte_alphabet_test_fake_target", + alphabet = "ë1092", + numbers = [1, 2, 3], + tags = ["manual"], + ) + repeating_alphabet_characters_test( + name = "repeating_alphabet_characters_test", + target_under_test = ":repeating_alphabet_characters_test_fake_target", + ) + hashed_file( + name = "repeating_alphabet_characters_test_fake_target", + alphabet = "aabcdefg", + numbers = [1, 2, 3], + tags = ["manual"], + ) + too_short_alphabet_test( + name = "too_short_alphabet_test", + target_under_test = ":too_short_alphabet_test_fake_target", + ) + hashed_file( + name = "too_short_alphabet_test_fake_target", + alphabet = "ab", + numbers = [1, 2, 3], + tags = ["manual"], + ) + unittest.suite( + name, + long_alphabet_test, + short_alphabet_test, + simple_test, + ) diff --git a/lib/tests/blocklist.bzl b/lib/tests/blocklist.bzl new file mode 100644 index 0000000..97287c5 --- /dev/null +++ b/lib/tests/blocklist.bzl @@ -0,0 +1,149 @@ +"Blocklist Tests" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//:defs.bzl", "decode", "encode", "sqids") + +def _default_blocklist_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + + numbers = [4572721] + id = "aho1e" + + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + asserts.equals(env, s.encode(numbers), "JExTR") + asserts.equals(env, encode(numbers), "JExTR") + + return unittest.end(env) + +default_blocklist_test = unittest.make(_default_blocklist_test_impl) + +def _empty_blocklist_test_impl(ctx): + env = unittest.begin(ctx) + + blocklist = [] + s = sqids(blocklist = blocklist) + + numbers = [4572721] + id = "aho1e" + + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, blocklist = blocklist), id) + + return unittest.end(env) + +empty_blocklist_test = unittest.make(_empty_blocklist_test_impl) + +def _custom_blocklist_test_impl(ctx): + env = unittest.begin(ctx) + + blocklist = ["ArUO"] + s = sqids(blocklist = blocklist) + + numbers = [4572721] + id = "aho1e" + blocked_id = "ArUO" + blocked_numbers = [100000] + + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, blocklist = blocklist), id) + + asserts.equals(env, s.decode(blocked_id), blocked_numbers) + asserts.equals(env, decode(blocked_id), blocked_numbers) + asserts.equals(env, s.encode(blocked_numbers), "QyG4") + asserts.equals(env, encode(blocked_numbers, blocklist = blocklist), "QyG4") + asserts.equals(env, s.decode("QyG4"), blocked_numbers) + asserts.equals(env, decode("QyG4"), blocked_numbers) + + return unittest.end(env) + +custom_blocklist_test = unittest.make(_custom_blocklist_test_impl) + +def _blocklist_test_impl(ctx): + env = unittest.begin(ctx) + + blocklist = [ + "JSwXFaosAN", # normal result of 1st encoding, block that word on purpose + "OCjV9JK64o", # result of 2nd encoding + "rBHf", # result of 3rd encoding is `4rBHfOiqd3`, let's block a substring + "79SM", # result of 4th encoding is `dyhgw479SM`, let's block the postfix + "7tE6", # result of 4th encoding is `7tE6jdAHLe`, let's block the prefix + ] + s = sqids(blocklist = blocklist) + + numbers = [1000000, 2000000] + id = "1aYeB7bRUt" + + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, blocklist = blocklist), id) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + + return unittest.end(env) + +blocklist_test = unittest.make(_blocklist_test_impl) + +def _decoding_blocklist_words_test_impl(ctx): + env = unittest.begin(ctx) + + blocklist = ["86Rf07", "se8ojk", "ARsz1p", "Q8AI49", "5sQRZO"] + numbers = [1, 2, 3] + s = sqids(blocklist = blocklist) + + for id in blocklist: + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + + return unittest.end(env) + +decoding_blocklist_words_test = unittest.make(_decoding_blocklist_words_test_impl) + +def _match_against_short_blocklist_word_test_impl(ctx): + env = unittest.begin(ctx) + + blocklist = ["pnd"] + s = sqids(blocklist = blocklist) + numbers = [1000] + + asserts.equals(env, s.decode(s.encode(numbers)), numbers) + asserts.equals(env, decode(encode(numbers, blocklist = blocklist)), numbers) + + return unittest.end(env) + +match_against_short_blocklist_word_test = unittest.make(_match_against_short_blocklist_word_test_impl) + +def _blocklist_filtering_test_impl(ctx): + env = unittest.begin(ctx) + + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + blocklist = ["sxnzkl"] + s = sqids(alphabet = alphabet, blocklist = blocklist) + numbers = [1, 2, 3] + id = "IBSHOZ" + + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, alphabet, blocklist), id) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id, alphabet), numbers) + + return unittest.end(env) + +blocklist_filtering_test = unittest.make(_blocklist_filtering_test_impl) + +def blocklist_test_suite(name = "blocklist_test_suite"): + unittest.suite( + name, + default_blocklist_test, + empty_blocklist_test, + custom_blocklist_test, + blocklist_test, + decoding_blocklist_words_test, + match_against_short_blocklist_word_test, + blocklist_filtering_test, + ) diff --git a/lib/tests/encoding.bzl b/lib/tests/encoding.bzl new file mode 100644 index 0000000..49ff698 --- /dev/null +++ b/lib/tests/encoding.bzl @@ -0,0 +1,187 @@ +"Encoding Tests" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("//:defs.bzl", "decode", "encode", "sqids") +load("//lib/tests:utils.bzl", "hashed_file") + +def _simple_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + numbers = [1, 2, 3] + id = "86Rf07" + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers), id) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + + return unittest.end(env) + +simple_test = unittest.make(_simple_test_impl) + +def _different_inputs_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + numbers = [0, 0, 0, 1, 2, 3, 100, 1000, 100000, 1000000] + asserts.equals(env, s.decode(s.encode(numbers)), numbers) + asserts.equals(env, decode(encode(numbers)), numbers) + + return unittest.end(env) + +different_inputs_test = unittest.make(_different_inputs_test_impl) + +def _incremental_numbers_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + ids = { + "bM": [0], + "Uk": [1], + "gb": [2], + "Ef": [3], + "Vq": [4], + "uw": [5], + "OI": [6], + "AX": [7], + "p6": [8], + "nJ": [9], + } + for id_str, numbers in ids.items(): + asserts.equals(env, s.encode(numbers), id_str) + asserts.equals(env, encode(numbers), id_str) + asserts.equals(env, s.decode(id_str), numbers) + asserts.equals(env, decode(id_str), numbers) + + return unittest.end(env) + +incremental_numbers_test = unittest.make(_incremental_numbers_test_impl) + +def _incremental_numbers_same_index_0_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + ids = { + "SvIz": [0, 0], + "n3qa": [0, 1], + "tryF": [0, 2], + "eg6q": [0, 3], + "rSCF": [0, 4], + "sR8x": [0, 5], + "uY2M": [0, 6], + "74dI": [0, 7], + "30WX": [0, 8], + "moxr": [0, 9], + } + for id_str, numbers in ids.items(): + asserts.equals(env, s.encode(numbers), id_str) + asserts.equals(env, encode(numbers), id_str) + asserts.equals(env, s.decode(id_str), numbers) + asserts.equals(env, decode(id_str), numbers) + + return unittest.end(env) + +incremental_numbers_same_index_0_test = unittest.make(_incremental_numbers_same_index_0_test_impl) + +def _incremental_numbers_same_index_1_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + ids = { + "SvIz": [0, 0], + "n3qa": [0, 1], + "tryF": [0, 2], + "eg6q": [0, 3], + "rSCF": [0, 4], + "sR8x": [0, 5], + "uY2M": [0, 6], + "74dI": [0, 7], + "30WX": [0, 8], + "moxr": [0, 9], + } + for id_str, numbers in ids.items(): + asserts.equals(env, s.encode(numbers), id_str) + asserts.equals(env, encode(numbers), id_str) + asserts.equals(env, s.decode(id_str), numbers) + asserts.equals(env, decode(id_str), numbers) + + return unittest.end(env) + +incremental_numbers_same_index_1_test = unittest.make(_incremental_numbers_same_index_1_test_impl) + +def _multi_input_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + numbers = list(range(100)) + asserts.equals(env, numbers, s.decode(s.encode(numbers))) + asserts.equals(env, numbers, decode(encode(numbers))) + + return unittest.end(env) + +multi_input_test = unittest.make(_multi_input_test_impl) + +def _encoding_no_numbers_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + asserts.equals(env, s.encode(), "") + asserts.equals(env, encode(), "") + + return unittest.end(env) + +encoding_no_numbers_test = unittest.make(_encoding_no_numbers_test_impl) + +def _decoding_empty_string_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + asserts.equals(env, s.decode(""), []) + asserts.equals(env, decode(""), []) + + return unittest.end(env) + +decoding_empty_string_test = unittest.make(_decoding_empty_string_test_impl) + +def _decoding_invalid_character_test_impl(ctx): + env = unittest.begin(ctx) + + s = sqids() + asserts.equals(env, s.decode("*"), []) + asserts.equals(env, decode("*"), []) + + return unittest.end(env) + +decoding_invalid_character_test = unittest.make(_decoding_invalid_character_test_impl) + +def _encoding_out_of_range_numbers_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Encoding supports numbers greater than 0") + + return analysistest.end(env) + +encoding_out_of_range_numbers_test = analysistest.make(_encoding_out_of_range_numbers_test_impl, expect_failure = True) + +def encoding_test_suite(name = "encoding_test_suite"): + encoding_out_of_range_numbers_test( + name = "encoding_out_of_range_numbers_test", + target_under_test = ":encoding_out_of_range_numbers_test_fake_target", + ) + hashed_file( + name = "encoding_out_of_range_numbers_test_fake_target", + numbers = [-1], + tags = ["manual"], + ) + unittest.suite( + name, + decoding_empty_string_test, + decoding_invalid_character_test, + different_inputs_test, + encoding_no_numbers_test, + incremental_numbers_same_index_0_test, + incremental_numbers_same_index_1_test, + incremental_numbers_test, + multi_input_test, + simple_test, + ) diff --git a/lib/tests/min_length.bzl b/lib/tests/min_length.bzl new file mode 100644 index 0000000..bc81035 --- /dev/null +++ b/lib/tests/min_length.bzl @@ -0,0 +1,156 @@ +"Min Length Tests" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("//:defs.bzl", "decode", "encode", "sqids") +load("//lib:constants.bzl", "DEFAULT_ALPHABET") +load("//lib/tests:utils.bzl", "hashed_file") + +def _simple_test_impl(ctx): + env = unittest.begin(ctx) + + min_length = len(DEFAULT_ALPHABET) + s = sqids(min_length = min_length) + + numbers = [1, 2, 3] + id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM" + + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, min_length = min_length), id) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + + return unittest.end(env) + +simple_test = unittest.make(_simple_test_impl) + +def _incremental_test_impl(ctx): + env = unittest.begin(ctx) + + numbers = [1, 2, 3] + map = { + 6: "86Rf07", + 7: "86Rf07x", + 8: "86Rf07xd", + 9: "86Rf07xd4", + 10: "86Rf07xd4z", + 11: "86Rf07xd4zB", + 12: "86Rf07xd4zBm", + 13: "86Rf07xd4zBmi", + } + + map[len(DEFAULT_ALPHABET) + 0] = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM" + map[len(DEFAULT_ALPHABET) + 1] = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy" + map[len(DEFAULT_ALPHABET) + 2] = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf" + map[len(DEFAULT_ALPHABET) + 3] = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1" + + for min_length, id in map.items(): + s = sqids(min_length = min_length) + + asserts.equals(env, s.encode(numbers), id) + asserts.equals(env, encode(numbers, min_length = min_length), id) + asserts.equals(env, len(s.encode(numbers)), min_length) + asserts.equals(env, len(encode(numbers, min_length = min_length)), min_length) + asserts.equals(env, s.decode(id), numbers) + asserts.equals(env, decode(id), numbers) + + return unittest.end(env) + +incremental_test = unittest.make(_incremental_test_impl) + +def _incremental_numbers_test_impl(ctx): + env = unittest.begin(ctx) + + min_length = len(DEFAULT_ALPHABET) + s = sqids(min_length = min_length) + + ids = { + "SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu": [0, 0], + "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc": [0, 1], + "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ": [0, 2], + "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE": [0, 3], + "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX": [0, 4], + "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2": [0, 5], + "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0": [0, 6], + "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy": [0, 7], + "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS": [0, 8], + "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin": [0, 9], + } + + for id_str, numbers in ids.items(): + asserts.equals(env, s.encode(numbers), id_str) + asserts.equals(env, encode(numbers, min_length = min_length), id_str) + asserts.equals(env, s.decode(id_str), numbers) + asserts.equals(env, decode(id_str), numbers) + + return unittest.end(env) + +incremental_numbers_test = unittest.make(_incremental_numbers_test_impl) + +def _min_lengths_test_impl(ctx): + env = unittest.begin(ctx) + + for min_length in [0, 1, 5, 10, len(DEFAULT_ALPHABET)]: + for numbers in [ + [0], + [0, 0, 0, 0, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [100, 200, 300], + [1000, 2000, 3000], + [1000000], + ]: + s = sqids(min_length = min_length) + + id_str = s.encode(numbers) + asserts.true(env, len(id_str) >= min_length) + asserts.equals(env, s.decode(id_str), numbers) + id_str = encode(numbers, min_length = min_length) + asserts.true(env, len(id_str) >= min_length) + asserts.equals(env, decode(id_str), numbers) + + return unittest.end(env) + +min_lengths_test = unittest.make(_min_lengths_test_impl) + +def _out_of_range_invalid_min_length_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Minimum length has to be between 0 and 255") + + return analysistest.end(env) + +out_of_range_invalid_min_length_test = analysistest.make(_out_of_range_invalid_min_length_test_impl, expect_failure = True) + +def min_length_test_suite(name = "min_length_test_suite"): + """Min Length Test Suite + + Args: + name: Name of the testsuite + """ + + out_of_range_invalid_min_length_test( + name = "out_of_range_invalid_min_length_test_1", + target_under_test = ":out_of_range_invalid_min_length_test_1_fake_target", + ) + hashed_file( + name = "out_of_range_invalid_min_length_test_1_fake_target", + min_length = -1, + numbers = [1, 2, 3], + tags = ["manual"], + ) + out_of_range_invalid_min_length_test( + name = "out_of_range_invalid_min_length_test_2", + target_under_test = ":out_of_range_invalid_min_length_test_2_fake_target", + ) + hashed_file( + name = "out_of_range_invalid_min_length_test_2_fake_target", + min_length = 256, + numbers = [1, 2, 3], + tags = ["manual"], + ) + unittest.suite( + name, + incremental_numbers_test, + incremental_test, + min_lengths_test, + simple_test, + ) diff --git a/lib/tests/utils.bzl b/lib/tests/utils.bzl new file mode 100644 index 0000000..0b1fd1e --- /dev/null +++ b/lib/tests/utils.bzl @@ -0,0 +1,25 @@ +"Test utils" + +load("//:defs.bzl", "sqids") +load("//lib:constants.bzl", "DEFAULT_ALPHABET", "DEFAULT_MIN_LENGTH") + +def _hashed_file_impl(ctx): + s = sqids(alphabet = ctx.attr.alphabet, min_length = ctx.attr.min_length) + hash = s.encode(ctx.attr.numbers) + out = ctx.actions.declare_file("%s-%s" % (ctx.attr.name, hash)) + + ctx.actions.write( + output = out, + content = "%s" % hash, + ) + + return [DefaultInfo(files = depset([out]))] + +hashed_file = rule( + _hashed_file_impl, + attrs = { + "alphabet": attr.string(default = DEFAULT_ALPHABET), + "min_length": attr.int(default = DEFAULT_MIN_LENGTH), + "numbers": attr.int_list(mandatory = True), + }, +)