diff options
author | EuAndreh <eu@euandre.org> | 2025-05-07 07:02:57 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2025-05-07 07:16:33 -0300 |
commit | d3cb41376ac0be31ed2b6649dd8d6b42674ebeaf (patch) | |
tree | df7eeb84a91114b6624a0f0fe7c1b11fa7e5ae1f | |
parent | tests/{cli-opts,integration}.sh: Add test implementations (diff) | |
download | uuid-d3cb41376ac0be31ed2b6649dd8d6b42674ebeaf.tar.gz uuid-d3cb41376ac0be31ed2b6649dd8d6b42674ebeaf.tar.xz |
Support UUIDv7, with -vN for choosing
-rw-r--r-- | deps.mk | 27 | ||||
-rw-r--r-- | doc/uuid.en.1.adoc | 42 | ||||
-rw-r--r-- | src/uuid.go | 171 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 106 | ||||
-rw-r--r-- | tests/functional/string-round-trip/uuid.go | 23 | ||||
l--------- | tests/fuzz/new-v4-from/main.go (renamed from tests/fuzz/new-from/main.go) | 0 | ||||
-rw-r--r-- | tests/fuzz/new-v4-from/uuid.go (renamed from tests/fuzz/new-from/uuid.go) | 2 | ||||
l--------- | tests/fuzz/new-v7-from/main.go | 1 | ||||
-rw-r--r-- | tests/fuzz/new-v7-from/uuid.go | 33 | ||||
-rw-r--r-- | tests/uuid.go | 387 |
10 files changed, 744 insertions, 48 deletions
@@ -3,7 +3,8 @@ libs.go = \ tests/benchmarks/string-roundtrip/uuid.go \ tests/functional/string-round-trip/uuid.go \ tests/fuzz/from-string/uuid.go \ - tests/fuzz/new-from/uuid.go \ + tests/fuzz/new-v4-from/uuid.go \ + tests/fuzz/new-v7-from/uuid.go \ tests/uuid.go \ mains.go = \ @@ -11,7 +12,8 @@ mains.go = \ tests/benchmarks/string-roundtrip/main.go \ tests/functional/string-round-trip/main.go \ tests/fuzz/from-string/main.go \ - tests/fuzz/new-from/main.go \ + tests/fuzz/new-v4-from/main.go \ + tests/fuzz/new-v7-from/main.go \ tests/main.go \ manpages.N.adoc = \ @@ -30,11 +32,13 @@ functional-tests/main.go = \ fuzz-targets/lib.go = \ tests/fuzz/from-string/uuid.go \ - tests/fuzz/new-from/uuid.go \ + tests/fuzz/new-v4-from/uuid.go \ + tests/fuzz/new-v7-from/uuid.go \ fuzz-targets/main.go = \ tests/fuzz/from-string/main.go \ - tests/fuzz/new-from/main.go \ + tests/fuzz/new-v4-from/main.go \ + tests/fuzz/new-v7-from/main.go \ benchmarks/lib.go = \ tests/benchmarks/string-roundtrip/uuid.go \ @@ -50,25 +54,30 @@ tests/functional/string-round-trip/main.a: tests/functional/string-round-trip/ma tests/functional/string-round-trip/uuid.a: tests/functional/string-round-trip/uuid.go tests/fuzz/from-string/main.a: tests/fuzz/from-string/main.go tests/fuzz/from-string/uuid.a: tests/fuzz/from-string/uuid.go -tests/fuzz/new-from/main.a: tests/fuzz/new-from/main.go -tests/fuzz/new-from/uuid.a: tests/fuzz/new-from/uuid.go +tests/fuzz/new-v4-from/main.a: tests/fuzz/new-v4-from/main.go +tests/fuzz/new-v4-from/uuid.a: tests/fuzz/new-v4-from/uuid.go +tests/fuzz/new-v7-from/main.a: tests/fuzz/new-v7-from/main.go +tests/fuzz/new-v7-from/uuid.a: tests/fuzz/new-v7-from/uuid.go tests/main.a: tests/main.go tests/uuid.a: tests/uuid.go src/main.bin: src/main.a tests/benchmarks/string-roundtrip/main.bin: tests/benchmarks/string-roundtrip/main.a tests/functional/string-round-trip/main.bin: tests/functional/string-round-trip/main.a tests/fuzz/from-string/main.bin: tests/fuzz/from-string/main.a -tests/fuzz/new-from/main.bin: tests/fuzz/new-from/main.a +tests/fuzz/new-v4-from/main.bin: tests/fuzz/new-v4-from/main.a +tests/fuzz/new-v7-from/main.bin: tests/fuzz/new-v7-from/main.a tests/main.bin: tests/main.a src/main.bin-check: src/main.bin tests/benchmarks/string-roundtrip/main.bin-check: tests/benchmarks/string-roundtrip/main.bin tests/functional/string-round-trip/main.bin-check: tests/functional/string-round-trip/main.bin tests/fuzz/from-string/main.bin-check: tests/fuzz/from-string/main.bin -tests/fuzz/new-from/main.bin-check: tests/fuzz/new-from/main.bin +tests/fuzz/new-v4-from/main.bin-check: tests/fuzz/new-v4-from/main.bin +tests/fuzz/new-v7-from/main.bin-check: tests/fuzz/new-v7-from/main.bin tests/main.bin-check: tests/main.bin src/main.a: src/$(NAME).a tests/benchmarks/string-roundtrip/main.a: tests/benchmarks/string-roundtrip/$(NAME).a tests/functional/string-round-trip/main.a: tests/functional/string-round-trip/$(NAME).a tests/fuzz/from-string/main.a: tests/fuzz/from-string/$(NAME).a -tests/fuzz/new-from/main.a: tests/fuzz/new-from/$(NAME).a +tests/fuzz/new-v4-from/main.a: tests/fuzz/new-v4-from/$(NAME).a +tests/fuzz/new-v7-from/main.a: tests/fuzz/new-v7-from/$(NAME).a tests/main.a: tests/$(NAME).a diff --git a/doc/uuid.en.1.adoc b/doc/uuid.en.1.adoc index 6366acb..dc6bc3e 100644 --- a/doc/uuid.en.1.adoc +++ b/doc/uuid.en.1.adoc @@ -4,12 +4,13 @@ == NAME -uuid - generate or validate a UUIDv4 +uuid - generate a UUID version 4 or 7, or validate a UUID string. == SYNOPSYS +*uuid* [-v (4|7)] *uuid* [_STRING_] @@ -17,16 +18,27 @@ uuid - generate or validate a UUIDv4 == EXAMPLES -=== Print a UUID on the terminal +=== Print a (version 4) UUID on the terminal +[source,sh] .... $ uuid d3891787-c952-af17-d697-0df3b85981e1 .... +=== Print a verson 7 UUID on the terminal + +[source,sh] +.... +$ uuid -v 7 +d3891787-c952-af17-d697-0df3b85981e1 +.... + + === Create a new UUID as part of a path +[source,sh] .... dir="$PWD"/"$(uuid)"/data .... @@ -34,6 +46,7 @@ dir="$PWD"/"$(uuid)"/data === Validate a given UUID +[source,sh] .... ID="$(basename -- "$directory")" if ! uuid "$ID"; then @@ -50,6 +63,17 @@ The *uuid* utility generates a new UUID when no _STRING_ is given and write it to _stdout_. If a _STRING_ is given, *uuid* validates it and exits, and produces no output. +The *-v* option allows for choosing which type of UUID to be emitted, either +version 4 ("the random one") or version 7 ("the sortable one"). When omitted, +version 4 is chosen as default. + + + +== OPTIONS + +*-v NUMBER*:: Choose the UUID version *NUMBER* to be generated. Supported +values are 4 (the default) and 7. + == OPERANDS @@ -75,12 +99,14 @@ Not used. The UUIDv4 format is made of byte blocks encoded as hexadecimals, using a <hyphen-minus>('`-`') as the block separator: +[source,sh] .... XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX .... An example output is: +[source,sh] .... cb46d0c0-b2aa-4a6c-a119-8deace2544a2 .... @@ -88,7 +114,15 @@ cb46d0c0-b2aa-4a6c-a119-8deace2544a2 === STDERR -Not used. +Errors on UUID validation are sent to the standard error. For instance: + +[source,sh] +.... +$ uuid not-a-uuid-123 1>/dev/null +uuid: str isn't of the correct length +.... + +When valid, validation emits nothing and exits with 0. === INPUT FILES @@ -111,7 +145,7 @@ None. == CONFORMING TO -The v4 UUID defined in *RFC 9562*. +The versions 4 and 7 of the UUID defined in *RFC 9562*. diff --git a/src/uuid.go b/src/uuid.go index 6c4b8f6..fa1e958 100644 --- a/src/uuid.go +++ b/src/uuid.go @@ -2,17 +2,34 @@ package uuid import ( "crypto/rand" + "encoding/binary" "encoding/hex" "errors" + "flag" "fmt" "io" "os" "strings" + "time" ) +type versionFlag byte const ( + versionFlag_v4 versionFlag = 0b01000000 + versionFlag_v7 versionFlag = 0b01110000 +) + +type actionType byte +const( + actionType_generate actionType = iota + actionType_parse +) + +const ( + variant0b10 = 0b10000000 + ByteCount = 16 dashCount = 4 encodedLength = (ByteCount * 2) + dashCount @@ -27,33 +44,85 @@ var ( ) ErrBadDashCount = errors.New("uuid: Bad count of dashes in string") ErrBadDashPosition = errors.New("uuid: Bad char in string") + + DefaultVersion = 4 + + Nil = UUID{ 0, 0, 0, 0, 0, 0, 0, 0, } + Max = UUID{ 255, 255, 255, 255, 255, 255, 255, 255, } ) type UUID [ByteCount]byte +type argsT struct{ + allArgs []string + subArgs []string + action actionType + version uint8 +} + -func NewFrom(r io.Reader) (UUID, error) { +func NewV4From(r io.Reader) (UUID, error) { var uuid UUID + _, err := io.ReadFull(r, uuid[:]) if err != nil { return UUID{}, err } - uuid[6] = (uuid[6] & 0x0f) | 0x40 // v4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // variant 10 + + uuid[6] = (uuid[6] & 0b00001111) | byte(versionFlag_v4) + uuid[8] = (uuid[8] & 0b00111111) | variant0b10 + return uuid, nil } -func New() UUID { - uuid, err := NewFrom(randomReader) +func NewV4() UUID { + uuid, err := NewV4From(randomReader) if err != nil { panic(err) } return uuid } +func NewV7From(r io.Reader, nowNanoFn func() uint64) (UUID, error) { + var uuid UUID + + now := uint64(nowNanoFn()) + binary.BigEndian.PutUint64(uuid[:8], now) + + _, err := io.ReadFull(r, uuid[8:]) + if err != nil { + return UUID{}, err + } + + uuid[6] = (uuid[6] & 0b00001111) | byte(versionFlag_v7) + uuid[8] = (uuid[8] & 0b00111111) | variant0b10 + + return uuid, nil +} + +func nowNano() uint64 { + return uint64(time.Now().UnixNano()) +} + +func NewV7() UUID { + uuid, err := NewV7From(randomReader, nowNano) + if err != nil { + panic(err) + } + return uuid +} + +func New() UUID { + if DefaultVersion == 4 { + return NewV4() + } else { + return NewV7() + } +} + func (uuid UUID) String() string { dst := [encodedLength]byte { 0, 0, 0, 0, @@ -103,16 +172,92 @@ func FromString(str string) (UUID, error) { return [ByteCount]byte(data), nil } +func usage(argv0 string, w io.Writer) { + fmt.Fprintf( + w, + ("Usage:\n" + + " %s [-v (4|7)]\n" + + " %s STRING\n"), + argv0, + argv0, + ) +} + +func actionForSubargs(subArgs []string) actionType { + if len(subArgs) == 0 { + return actionType_generate + } else { + return actionType_parse + } +} + +func getopt(allArgs []string, w io.Writer) (argsT, int) { + argv0 := allArgs[0] + argv := allArgs[1:] + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Usage = func() {} + fs.SetOutput(w) + versionNumber := fs.Uint( + "v", + 4, + "which UUID version to generate", + ) -func Main() { - if len(os.Args) < 2 { - fmt.Println(New().String()) + if fs.Parse(argv) != nil { + usage(argv0, w) + return argsT{}, 2 + } + + subArgs := fs.Args() + actionType := actionForSubargs(subArgs) + + var version uint8 + if *versionNumber == 4 || *versionNumber == 7 { + version = uint8(*versionNumber) } else { - _, err := FromString(strings.TrimSpace(os.Args[1])) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(3) - } + fmt.Fprintf(w, "Bad value for VERSION: %s.\n", *versionNumber) + usage(argv0, w) + return argsT{}, 2 + } + + return argsT{ + allArgs: allArgs, + subArgs: subArgs, + action: actionType, + version: version, + }, 0 +} + +func run(args argsT, _ io.Reader, stdout io.Writer, stderr io.Writer) int { + switch args.action { + case actionType_generate: + if args.version == 4 { + fmt.Fprintf(stdout, "%v\n", NewV4().String()) + } else if args.version == 7 { + fmt.Fprintf(stdout, "%v\n", NewV7().String()) + } + return 0 + + case actionType_parse: + _, err := FromString(strings.TrimSpace(args.subArgs[0])) + if err != nil { + fmt.Fprintln(stderr, err) + return 3 + } else { + return 0 + } + } + + return 126 +} + + + +func Main() { + args, rc := getopt(os.Args, os.Stderr) + if rc != 0 { + os.Exit(rc) } + os.Exit(run(args, os.Stdin, os.Stdout, os.Stderr)) } diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh index 39f01f4..1d7cab8 100755 --- a/tests/cli-opts.sh +++ b/tests/cli-opts.sh @@ -4,6 +4,85 @@ set -eu . tests/lib.sh +test_unsupported_flags() { + testing 'unsupported flags' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./uuid.bin -x 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_grep_stderr '^Usage:$' + rm "$OUT" "$ERR" + + test_ok +} + +test_flag_without_required_arguments() { + testing 'flag without required arguments' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./uuid.bin -v 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_grep_stderr '^Usage:$' + rm "$OUT" "$ERR" + + test_ok +} + +test_flag_with_bad_argument_value() { + testing 'flag without required arguments' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./uuid.bin -v 1 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_grep_stderr '^Usage:$' + rm "$OUT" "$ERR" + + test_ok +} + +test_generates_uuid_with_custom_version() { + testing 'UUID with custom version' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./uuid.bin -v 4 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 0 + assert_grep_stdout '^.{,14}4' + assert_empty_stderr + rm "$OUT" "$ERR" + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./uuid.bin -v 7 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 0 + assert_grep_stdout '^.{,14}7' + assert_empty_stderr + rm "$OUT" "$ERR" + + test_ok +} + test_generates_uuid_with_0_args() { testing 'generates UUID with 0 arguments' @@ -28,7 +107,8 @@ test_checks_string_with_1_arg() { ERR="$(mkstemp)" trap 'rm -f "$OUT" "$ERR"' EXIT STATUS=0 - ./uuid.bin 'cac94e13-41fa-40c4-bd46-5b7b3b46c09e' 1>"$OUT" 2>"$ERR" || STATUS=$? + ID='cac94e13-41fa-40c4-bd46-5b7b3b46c09e' + ./uuid.bin "$ID" 1>"$OUT" 2>"$ERR" || STATUS=$? assert_status 0 assert_empty_stdout assert_empty_stderr @@ -48,7 +128,31 @@ test_checks_string_with_1_arg() { test_ok } +test_checks_string_with_1_arg_ignores_options() { + testing 'checks string with 1 arg ignores options' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ID='cac94e13-41fa-40c4-bd46-5b7b3b46c09e' + ID='76600ea2-0282-4b38-9bc9-dd69125445f3' + ./uuid.bin -v 4 "$ID" 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 0 + assert_empty_stdout + assert_empty_stderr + rm "$OUT" "$ERR" + + test_ok +} + +test_unsupported_flags +test_flag_without_required_arguments test_generates_uuid_with_0_args +test_flag_with_bad_argument_value +test_generates_uuid_with_custom_version test_checks_string_with_1_arg +test_checks_string_with_1_arg_ignores_options diff --git a/tests/functional/string-round-trip/uuid.go b/tests/functional/string-round-trip/uuid.go index 000d3fb..82414ea 100644 --- a/tests/functional/string-round-trip/uuid.go +++ b/tests/functional/string-round-trip/uuid.go @@ -45,16 +45,31 @@ func assertEq(given any, expected any) { func MainTest() { - testing("string is the same after round-trip", func() { - str1 := New().String() + testing("v4 string is the same after round-trip", func() { + str1 := NewV4().String() id, err := FromString(str1) assertEq(err, nil) str2 := id.String() assertEq(str1, str2) }) - testing("UUID is the same after round-trip", func() { - id1 := New() + testing("v4 UUID is the same after round-trip", func() { + id1 := NewV4() + id2, err := FromString(id1.String()) + assertEq(err, nil) + assertEq(id1, id2) + }) + + testing("v7 string is the same after round-trip", func() { + str1 := NewV7().String() + id, err := FromString(str1) + assertEq(err, nil) + str2 := id.String() + assertEq(str1, str2) + }) + + testing("v7 UUID is the same after round-trip", func() { + id1 := NewV7() id2, err := FromString(id1.String()) assertEq(err, nil) assertEq(id1, id2) diff --git a/tests/fuzz/new-from/main.go b/tests/fuzz/new-v4-from/main.go index f67563d..f67563d 120000 --- a/tests/fuzz/new-from/main.go +++ b/tests/fuzz/new-v4-from/main.go diff --git a/tests/fuzz/new-from/uuid.go b/tests/fuzz/new-v4-from/uuid.go index 298687b..90cc994 100644 --- a/tests/fuzz/new-from/uuid.go +++ b/tests/fuzz/new-v4-from/uuid.go @@ -11,7 +11,7 @@ import ( func fn(f *testing.F) { f.Fuzz(func(t *testing.T, payload []byte) { - NewFrom(bytes.NewReader(payload)) + NewV4From(bytes.NewReader(payload)) }) } diff --git a/tests/fuzz/new-v7-from/main.go b/tests/fuzz/new-v7-from/main.go new file mode 120000 index 0000000..f67563d --- /dev/null +++ b/tests/fuzz/new-v7-from/main.go @@ -0,0 +1 @@ +../../main.go
\ No newline at end of file diff --git a/tests/fuzz/new-v7-from/uuid.go b/tests/fuzz/new-v7-from/uuid.go new file mode 100644 index 0000000..8686ebf --- /dev/null +++ b/tests/fuzz/new-v7-from/uuid.go @@ -0,0 +1,33 @@ +package uuid + +import ( + "bytes" + "os" + "testing" + "testing/internal/testdeps" +) + + + +func fn(f *testing.F) { + f.Fuzz(func(t *testing.T, payload []byte, nanosecs uint64) { + NewV7From(bytes.NewReader(payload), func() uint64 { + return nanosecs + }) + }) +} + + + +func MainTest() { + fuzzTargets := []testing.InternalFuzzTarget{ + { "fn", fn }, + } + + deps := testdeps.TestDeps{} + tests := []testing.InternalTest {} + benchmarks := []testing.InternalBenchmark{} + examples := []testing.InternalExample {} + m := testing.MainStart(deps, tests, benchmarks, fuzzTargets, examples) + os.Exit(m.Run()) +} diff --git a/tests/uuid.go b/tests/uuid.go index ad9cac4..912339a 100644 --- a/tests/uuid.go +++ b/tests/uuid.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "fmt" + "io" "os" "reflect" "strings" @@ -50,18 +51,24 @@ func assertEq(given any, expected any) { } -func test_NewFrom() { - testStart("NewFrom()") + +func test_NewV4From() { + testStart("NewV4From()") + + testing("we propagate the error when it happens", func() { + _, err := NewV4From(strings.NewReader("")) + assertEq(err, io.EOF) + }) testing("we get the same UUID from the same input", func() { const s = "abcdefghijklmnop" r1 := strings.NewReader(s) - uuid1, err := NewFrom(r1) + uuid1, err := NewV4From(r1) assertEq(err, nil) r2 := strings.NewReader(s) - uuid2, err := NewFrom(r2) + uuid2, err := NewV4From(r2) assertEq(err, nil) assertEq(uuid1, uuid2) @@ -107,7 +114,7 @@ func test_NewFrom() { } r := bytes.NewReader(input) - given, err := NewFrom(r) + given, err := NewV4From(r) assertEq(err, nil) assertEq(given, expected) }) @@ -152,30 +159,166 @@ func test_NewFrom() { } r := bytes.NewReader(input) - given, err := NewFrom(r) + given, err := NewV4From(r) assertEq(err, nil) assertEq(given, expected) }) } -func test_New() { - testStart("New()") +func test_NewV4() { + testStart("NewV4()") testing("we can generate UUID values: ", func() { - var uuid UUID = New() - assertEq(len(uuid), 16) + var uuid UUID = NewV4() + assertEq(uuid == Nil, false) + assertEq(uuid == Max, false) }) testing("panic when the randomReader fails", func() { savedReader := randomReader - randomReader = strings.NewReader("abc") + randomReader = strings.NewReader("") + defer func() { + r := recover() + assertEq(r, io.EOF) + randomReader = savedReader + }() + + NewV4() + os.Exit(5) + }) +} + +func test_NewV7From() { + testStart("NewV7From()") + + testing("reader error is propagated", func() { + nowFn := func() uint64 { + return 0 + } + + _, err := NewV7From(strings.NewReader(""), nowFn) + assertEq(err, io.EOF) + }) + + testing("we get the same UUID given the same input", func() { + const s = "abcdefgh" + nowFn := func() uint64 { + return 0 + } + + r1 := strings.NewReader(s) + uuid1, err := NewV7From(r1, nowFn) + assertEq(err, nil) + + r2 := strings.NewReader(s) + uuid2, err := NewV7From(r2, nowFn) + assertEq(err, nil) + + assertEq(uuid1, uuid2) + }) + + testing("the bytes are what the reader plus the time gives", func() { + randomInput := []byte{ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + } + + nowFn := func() uint64 { + return 0xffffffff00000000 + } + + expected := UUID{ + 0xff, + 0xff, + 0xff, + 0xff, + 0x00, + 0x00, + 0x00 + 0x70, + 0x00, + 0x00 + 0x80, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + } + + r := bytes.NewReader(randomInput) + given, err := NewV7From(r, nowFn) + assertEq(err, nil) + assertEq(given, expected) + }) + + testing("v7 and variant markers", func() { + randomInput := []byte{ + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + } + + nowFn := func() uint64 { + return 0x1111111111111111 + } + + expected := UUID{ + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x71, // not 0x11 + 0x11, + 0x91, // not 0x11 + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + } + + r := bytes.NewReader(randomInput) + given, err := NewV7From(r, nowFn) + assertEq(err, nil) + assertEq(given, expected) + }) +} + +func test_NewV7() { + testStart("NewV7()") + + testing("we can generate UUID values: ", func() { + var uuid UUID = NewV7() + assertEq(uuid == Nil, false) + assertEq(uuid == Max, false) + }) + + testing("panic when reader fails", func() { + savedReader := randomReader + randomReader = strings.NewReader("") defer func() { r := recover() - assertEq(r == nil, false) + assertEq(r, io.EOF) randomReader = savedReader }() - New() + NewV7() os.Exit(5) }) } @@ -224,7 +367,7 @@ func test_FromString() { testing("UUID -> string -> UUID round trip", func() { for i := 0; i < 100; i++ { - uuid0 := New() + uuid0 := NewV4() uuid1, err := FromString(uuid0.String()) assertEq(err, nil) assertEq(uuid0, uuid1) @@ -251,11 +394,223 @@ func test_FromString() { }) } +func test_usage() { + testStart("usage()") + + testing("all it does is write to the given io.Writer", func() { + w := strings.Builder{} + usage("xxx", &w) + + const expectedRaw = ` + Usage: + xxx [-v (4|7)] + xxx STRING + ` + expected := strings.Trim( + strings.ReplaceAll(expectedRaw, "\t", ""), + "\n", + ) + "\n" + assertEq(w.String(), expected) + }) +} + +func test_actionForSubargs() { + testStart("actionForSubargs()") + + testing("we decide based only on length", func() { + assertEq(actionForSubargs([]string{}), actionType_generate) + assertEq(actionForSubargs([]string{""}), actionType_parse) + assertEq(actionForSubargs([]string{"id"}), actionType_parse) + }) +} + +func test_getopt() { + testStart("getopt()") + + const usageRaw = ` + Usage: + $0 [-v (4|7)] + $0 STRING + ` + usage := strings.Trim( + strings.ReplaceAll(usageRaw, "\t", ""), + "\n", + ) + "\n" + + testing("we supress the default error message", func() { + w := strings.Builder{} + argv := []string{"$0", "-h"} + _, rc := getopt(argv, &w) + + assertEq(w.String(), usage) + assertEq(rc, 2) + }) + + testing("we get unsupported flag error", func() { + w := strings.Builder{} + argv := []string{"$0", "-Z"} + _, rc := getopt(argv, &w) + + const message = "flag provided but not defined: -Z\n" + assertEq(w.String(), message + usage) + assertEq(rc, 2) + }) + + testing("we get incorrect use of flag error", func() { + w := strings.Builder{} + argv := []string{"$0", "-v"} + _, rc := getopt(argv, &w) + + const message = "flag needs an argument: -v\n" + assertEq(w.String(), message + usage) + assertEq(rc, 2) + }) + + testing("we get bad flag value error", func() { + w := strings.Builder{} + argv := []string{"$0", "-v", "a"} + _, rc := getopt(argv, &w) + + const message = "invalid value \"a\" for flag -v: parse error\n" + assertEq(w.String(), message + usage) + assertEq(rc, 2) + }) + + testing("the args has the picked version", func() { + var ( + w1 = strings.Builder{} + w2 = strings.Builder{} + w3 = strings.Builder{} + ) + + argsIn1 := []string{"$0"} + argsIn2 := []string{"$0", "-v", "4"} + argsIn3 := []string{"$0", "-v", "7"} + + args1, rc1 := getopt(argsIn1, &w1) + args2, rc2 := getopt(argsIn2, &w2) + args3, rc3 := getopt(argsIn3, &w3) + + expected1 := argsT{ + allArgs: []string{"$0"}, + subArgs: []string{}, + action: actionType_generate, + version: 4, + } + expected2 := argsT{ + allArgs: []string{"$0", "-v", "4"}, + subArgs: []string{}, + action: actionType_generate, + version: 4, + } + expected3 := argsT{ + allArgs: []string{"$0", "-v", "7"}, + subArgs: []string{}, + action: actionType_generate, + version: 7, + } + + assertEq(w1.String(), "") + assertEq(w2.String(), "") + assertEq(w3.String(), "") + assertEq(rc1, 0) + assertEq(rc2, 0) + assertEq(rc3, 0) + assertEq(args1, expected1) + assertEq(args2, expected2) + assertEq(args3, expected3) + }) + + testing("the args has the picked action", func() { + var ( + w1 = strings.Builder{} + w2 = strings.Builder{} + ) + + argsIn1 := []string{"$0", "the-string"} + argsIn2 := []string{"$0", "-v", "7", "the-string"} + + args1, rc1 := getopt(argsIn1, &w1) + args2, rc2 := getopt(argsIn2, &w2) + + expected1 := argsT{ + allArgs: []string{"$0", "the-string"}, + subArgs: []string{"the-string"}, + action: actionType_parse, + version: 4, + } + expected2 := argsT{ + allArgs: []string{"$0", "-v", "7", "the-string"}, + subArgs: []string{"the-string"}, + action: actionType_parse, + version: 7, + } + + assertEq(w1.String(), "") + assertEq(w2.String(), "") + assertEq(rc1, 0) + assertEq(rc2, 0) + assertEq(args1, expected1) + assertEq(args2, expected2) + }) +} + +func test_run() { + testStart("run()") + + testing("generating IDs gives us 0 rc", func() { + out1 := strings.Builder{} + out2 := strings.Builder{} + args1 := argsT{ + action: actionType_generate, + version: 4, + } + args2 := argsT{ + action: actionType_generate, + version: 7, + } + + rc1 := run(args1, nil, &out1, nil) + rc2 := run(args2, nil, &out2, nil) + + assertEq(rc1, 0) + assertEq(rc2, 0) + assertEq(len(out1.String()), encodedLength + len("\n")) + assertEq(len(out2.String()), encodedLength + len("\n")) + }) + + testing("parsing varies based on ID validity", func() { + const id = "ggggggg-ggggg-gggg-gggg-gggggggggggg" + err1 := strings.Builder{} + args1 := argsT{ + action: actionType_parse, + subArgs: []string{ id }, + } + args2 := argsT{ + action: actionType_parse, + subArgs: []string{New().String()}, + } + + rc1 := run(args1, nil, nil, &err1) + rc2 := run(args2, nil, nil, nil) + + assertEq(rc1, 3) + assertEq(rc2, 0) + assertEq(err1.String(), "uuid: Bad char in string\n") + }) +} + func MainTest() { - test_NewFrom() - test_New() + test_NewV4From() + test_NewV4() + test_NewV7From() + test_NewV7() test_String() test_FromString() + test_usage() + test_actionForSubargs() + test_getopt() + test_run() } |