summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2025-05-07 07:02:57 -0300
committerEuAndreh <eu@euandre.org>2025-05-07 07:16:33 -0300
commitd3cb41376ac0be31ed2b6649dd8d6b42674ebeaf (patch)
treedf7eeb84a91114b6624a0f0fe7c1b11fa7e5ae1f
parenttests/{cli-opts,integration}.sh: Add test implementations (diff)
downloaduuid-d3cb41376ac0be31ed2b6649dd8d6b42674ebeaf.tar.gz
uuid-d3cb41376ac0be31ed2b6649dd8d6b42674ebeaf.tar.xz
Support UUIDv7, with -vN for choosing
-rw-r--r--deps.mk27
-rw-r--r--doc/uuid.en.1.adoc42
-rw-r--r--src/uuid.go171
-rwxr-xr-xtests/cli-opts.sh106
-rw-r--r--tests/functional/string-round-trip/uuid.go23
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.go1
-rw-r--r--tests/fuzz/new-v7-from/uuid.go33
-rw-r--r--tests/uuid.go387
10 files changed, 744 insertions, 48 deletions
diff --git a/deps.mk b/deps.mk
index 21edbee..3e780fe 100644
--- a/deps.mk
+++ b/deps.mk
@@ -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()
}