From 779818e06799453649148a65e079bf2fea213527 Mon Sep 17 00:00:00 2001 From: andrey1s Date: Fri, 4 Oct 2024 14:55:21 +0300 Subject: [PATCH] init --- .drone.yml | 15 ++ .golangci.yml | 50 +++++++ example_test.go | 76 ++++++++++ go.mod | 3 + size.go | 357 ++++++++++++++++++++++++++++++++++++++++++++++ size_json.go | 9 ++ size_test.go | 190 ++++++++++++++++++++++++ size_text.go | 20 +++ size_text_test.go | 55 +++++++ 9 files changed, 775 insertions(+) create mode 100644 .drone.yml create mode 100644 .golangci.yml create mode 100644 example_test.go create mode 100644 go.mod create mode 100644 size.go create mode 100644 size_json.go create mode 100644 size_test.go create mode 100644 size_text.go create mode 100644 size_text_test.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..f26b3c2 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,15 @@ +--- +kind: pipeline +name: default + +steps: +- name: test + image: golang + commands: + # - go test -parallel 10 -race ./... + - go test ./... + +- name: golangci-lint + image: golangci/golangci-lint:v1.61 + commands: + - golangci-lint run \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..fe51531 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,50 @@ +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 60 + goconst: + min-len: 2 + min-occurrences: 2 + gocyclo: + min-complexity: 15 + golint: + min-confidence: 0 + lll: + line-length: 140 + maligned: + suggest-new: true + misspell: + locale: US + varnamelen: + min-name-length: 2 + ignore-decls: + - w io.Writer + - t testing.T + - e error + - i int + - b bytes.Buffer + - h Handle + +linters: + enable-all: true + disable: + - gochecknoglobals + - ireturn + - mnd + - nolintlint + # deprecated + - gomnd + - exportloopref + - execinquery + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gochecknoglobals + - depguard + - gosec + - errchkjson diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..f665378 --- /dev/null +++ b/example_test.go @@ -0,0 +1,76 @@ +package bytesize_test + +import ( + "encoding/json" + "fmt" + + "gitoa.ru/go-4devs/bytesize" +) + +func ExampleParse() { + size, err := bytesize.Parse("100kB") + fmt.Printf("100kB:%[1]T(%[1]d),%[2]v\n", size, err) + + size, err = bytesize.Parse("100PB") + fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err) + + size, err = bytesize.Parse("100KiB") + fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err) + + size, err = bytesize.Parse("100MiB") + fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err) + + size, err = bytesize.Parse("100B") + fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err) + + // Output: + // 100kB:bytesize.Size(100000), + // 100PB:bytesize.Size(100000000000000000), + // 100PB:bytesize.Size(102400), + // 100PB:bytesize.Size(104857600), + // 100PB:bytesize.Size(100), +} + +func ExampleSize_String() { + fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100000)) + fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100000000000000000)) + fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(102400)) + fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(104857600)) + fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100)) + + // Output: + // bytesize.Size(100000) = 100kB + // bytesize.Size(100000000000000000) = 100PB + // bytesize.Size(102400) = 102.4kB + // bytesize.Size(104857600) = 104.8576MB + // bytesize.Size(100) = 100B +} + +func ExampleSize_jsonMarshal() { + size := bytesize.Size(100000) + data, _ := json.Marshal(size) + fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data) + + size = bytesize.Size(100000000000000000) + data, _ = json.Marshal(size) + fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data) + + size = bytesize.Size(102400) + data, _ = json.Marshal(size) + fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data) + + size = bytesize.Size(104857600) + data, _ = json.Marshal(size) + fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data) + + size = bytesize.Size(100) + data, _ = json.Marshal(size) + fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data) + + // Output: + // bytesize.Size(100000) = "100kB" + // bytesize.Size(100000000000000000) = "100PB" + // bytesize.Size(102400) = "102.4kB" + // bytesize.Size(104857600) = "104.8576MB" + // bytesize.Size(100) = "100B" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c539ad1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitoa.ru/go-4devs/bytesize + +go 1.22.5 diff --git a/size.go b/size.go new file mode 100644 index 0000000..48c8834 --- /dev/null +++ b/size.go @@ -0,0 +1,357 @@ +package bytesize + +import ( + "errors" + "fmt" +) + +type Size int64 + +// Byte size prefix. +const ( + Byte Size = 1 // byte + Kibibyte Size = 1 << (10 * iota) // kibibyte + Mebibyte // mebibyte + Gibibyte // gibibyte + Tebibyte // tebibyte + Pebibyte // pebibyte + + Kilobyte Size = 1000 * Byte // kilobyte + Megabyte Size = 1000 * Kilobyte // megabyte + Gigabyte Size = 1000 * Megabyte // gigabyte + Terabyte Size = 1000 * Gigabyte // terabyte + Petabyte Size = 1000 * Terabyte // petabyte +) + +var unitMap = map[string]uint64{ + "B": uint64(Byte), + "KiB": uint64(Kibibyte), + "MiB": uint64(Mebibyte), + "GiB": uint64(Gibibyte), + "TiB": uint64(Tebibyte), + "PiB": uint64(Pebibyte), + + "kB": uint64(Kilobyte), + "KB": uint64(Kilobyte), + "MB": uint64(Megabyte), + "GB": uint64(Gigabyte), + "TB": uint64(Terabyte), + "PB": uint64(Petabyte), +} + +var ( + ErrInvalidSize = errors.New("bytesize: invalid size") + ErrMissingUnit = errors.New("bytesize: missing unit") + ErrUnknownUnit = errors.New("bytesize: unknown unit") +) + +// Parse parses a size string. +// A byte size string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300kB", "-1.5GiB" or "2GB45MB". +// Valid units are "B", "KiB", "MiB", "GiB", "TiB", "PiB" for binary size. +// Valid units are "B", "kB", "MB", "GB", "TB", "PB" for human size. +// +// nolint: funlen,gocognit,gocyclo,cyclop +func Parse(in string) (Size, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := in + neg := false + + var res uint64 + + // Consume [-+]? + if in != "" { + c := in[0] + if c == '-' || c == '+' { + neg = c == '-' + in = in[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if in == "0" { + return 0, nil + } + + if in == "" { + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + for in != "" { + var ( + val, fVal uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(in[0] == '.' || '0' <= in[0] && in[0] <= '9') { + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + // Consume [0-9]* + pl := len(in) + + val, in, err = leadingInt(in) + if err != nil { + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + pre := pl != len(in) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + + if in != "" && in[0] == '.' { + in = in[1:] + pl := len(in) + fVal, scale, in = leadingFraction(in) + post = pl != len(in) + } + + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + // Consume unit. + i := 0 + for ; i < len(in); i++ { + c := in[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + + if i == 0 { + return 0, fmt.Errorf("%w %q", ErrMissingUnit, orig) + } + + u := in[:i] + in = in[i:] + unit, ok := unitMap[u] + + if !ok { + return 0, fmt.Errorf("%w %s in size %q", ErrMissingUnit, u, orig) + } + + if val > 1<<63/unit { + // overflow + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + val *= unit + if fVal > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + val += uint64(float64(fVal) * (float64(unit) / scale)) + if val > 1<<63 { + // overflow + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + } + + res += val + if res > 1<<63 { + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + } + + if neg { + return -Size(res), nil + } + + if res > 1<<63-1 { + return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig) + } + + return Size(res), nil +} + +// leadingFraction consumes the leading [0-9]* from s. +// It is used only for fractions, so does not return an error on overflow, +// it just stops accumulating precision. +func leadingFraction(in string) (uint64, float64, string) { + i := 0 + scale := float64(1) + overflow := false + + var res uint64 + + for ; i < len(in); i++ { + cur := in[i] + if cur < '0' || cur > '9' { + break + } + + if overflow { + continue + } + + if res > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + + continue + } + + pres := res*10 + uint64(cur) - '0' + if pres > 1<<63 { + overflow = true + + continue + } + + res = pres + scale *= 10 + } + + return res, scale, in[i:] +} + +var errLeadingInt = errors.New("bytesize: bad [0-9]*") + +// leadingInt consumes the leading [0-9]* from in. +func leadingInt[bytes []byte | string](in bytes) (uint64, bytes, error) { + i := 0 + + var ( + res uint64 + rem bytes + ) + + for ; i < len(in); i++ { + curr := in[i] + + if curr < '0' || curr > '9' { + break + } + + if res > 1<<63/10 { + // overflow + return 0, rem, errLeadingInt + } + + res = res*10 + uint64(curr) - '0' + if res > 1<<63 { + // overflow + return 0, rem, errLeadingInt + } + } + + return res, in[i:], nil +} + +func (s Size) String() string { + var arr [32]byte + n := s.format(&arr) + + return string(arr[n:]) +} + +// format formats the representation of d into the end of buf and +// returns the offset of the first character. +// nolint: gosec +func (s Size) format(buf *[32]byte) int { + wLen := len(buf) + + data := uint64(s) + neg := s < 0 + + if neg { + data = -data + } + + prec := 3 + wLen-- + buf[wLen] = 'B' + wLen-- + + switch { + case data == 0: + buf[wLen] = '0' + + return wLen + case data < uint64(Kilobyte): + // print bytes + wLen++ + prec = 0 + case data < uint64(Megabyte): + // print kipobytes + buf[wLen] = 'k' + case data < uint64(Gigabyte): + // print megabytes + prec = 6 + buf[wLen] = 'M' + case data < uint64(Terabyte): + // print gigabytes + prec = 9 + buf[wLen] = 'G' + case data < uint64(Petabyte): + // print terabytes + prec = 12 + buf[wLen] = 'T' + default: + // print petobytes + prec = 15 + buf[wLen] = 'P' + } + + wLen, data = fmtFrac(buf[:wLen], data, prec) + wLen = fmtInt(buf[:wLen], data) + + if neg { + wLen-- + buf[wLen] = '-' + } + + return wLen +} + +// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the +// tail of buf, omitting trailing zeros. It omits the decimal +// point too when the fraction is 0. It returns the index where the +// output bytes begin and the value v/10**prec. +func fmtFrac(buf []byte, val uint64, prec int) (int, uint64) { + // Omit trailing zeros up to and including decimal point. + wLen := len(buf) + show := false + + for range prec { + digit := val % 10 + show = show || digit != 0 + + if show { + wLen-- + buf[wLen] = byte(digit) + '0' + } + + val /= 10 + } + + if show { + wLen-- + buf[wLen] = '.' + } + + return wLen, val +} + +// fmtInt formats v into the tail of buf. +// It returns the index where the output begins. +func fmtInt(buf []byte, val uint64) int { + wLen := len(buf) + if val == 0 { + wLen-- + buf[wLen] = '0' + } else { + for val > 0 { + wLen-- + buf[wLen] = byte(val%10) + '0' + val /= 10 + } + } + + return wLen +} diff --git a/size_json.go b/size_json.go new file mode 100644 index 0000000..a3428c5 --- /dev/null +++ b/size_json.go @@ -0,0 +1,9 @@ +package bytesize + +// func (s Size) MarshalJSON() ([]byte, error) { +// return nil, nil +// } + +// func (s *Size) UnmarshalJSON([]byte) error { +// return nil +// } diff --git a/size_test.go b/size_test.go new file mode 100644 index 0000000..e766acd --- /dev/null +++ b/size_test.go @@ -0,0 +1,190 @@ +package bytesize_test + +import ( + "math" + "math/rand" + "strings" + "testing" + + "gitoa.ru/go-4devs/bytesize" +) + +var parseTests = []struct { + in string + want bytesize.Size +}{ + // simple + {"0", 0}, + {"5KiB", 5 * bytesize.Kibibyte}, + {"30KiB", 30 * bytesize.Kibibyte}, + {"1478KiB", 1478 * bytesize.Kibibyte}, + // sign + {"-5KiB", -5 * bytesize.Kibibyte}, + {"+5KiB", 5 * bytesize.Kibibyte}, + {"-0", 0}, + {"+0", 0}, + // decimal + {"5.0KiB", 5 * bytesize.Kibibyte}, + {"5.6KiB", 5*bytesize.Kibibyte + bytesize.Kibibyte*6/10}, + {"5.KiB", 5 * bytesize.Kibibyte}, + {".5KiB", bytesize.Kibibyte / 2}, + {"1.0KiB", 1 * bytesize.Kibibyte}, + {"1.00KiB", 1 * bytesize.Kibibyte}, + {"1.004KiB", 1*bytesize.Kibibyte + 4*bytesize.Byte}, + {"1.0040KiB", 1*bytesize.Kibibyte + 4*bytesize.Byte}, + {"100.00100KiB", 100*bytesize.Kibibyte + 1*bytesize.Byte}, + // different units + {"10B", 10 * bytesize.Byte}, + {"11KiB", 11 * bytesize.Kibibyte}, + {"12MiB", 12 * bytesize.Mebibyte}, + {"12GiB", 12 * bytesize.Gibibyte}, + {"13TiB", 13 * bytesize.Tebibyte}, + {"14PiB", 14 * bytesize.Pebibyte}, + // composite durations + {"3PiB30TiB", 3*bytesize.Pebibyte + 30*bytesize.Tebibyte}, + {"10.5MiB4TiB", 4*bytesize.Tebibyte + 10*bytesize.Mebibyte + bytesize.Mebibyte/2}, + {"-2TiB3.4MiB", -(2*bytesize.Tebibyte + 3*bytesize.Mebibyte + bytesize.Mebibyte*4/10)}, + { + "1PiB2TiB3GiB4MiB5KiB6B", + 1*bytesize.Pebibyte + 2*bytesize.Tebibyte + 3*bytesize.Gibibyte + 4*bytesize.Mebibyte + 5*bytesize.Kibibyte + 6*bytesize.Byte, + }, + { + "39PiB9TiB14.425GiB", + 39*bytesize.Pebibyte + 9*bytesize.Tebibyte + 14*bytesize.Gibibyte + bytesize.Gibibyte*425/1000, + }, + // large value + {"52763797000B", 52763797000 * bytesize.Byte}, + // more than 9 digits after decimal point + {"0.3333333333333333333GiB", bytesize.Gibibyte * 3333333333 / 1e10}, + // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 + {"9007199254740993B", (1<<53 + 1) * bytesize.Byte}, + // largest duration that can be represented by int64 in nanoseconds + {"9223372036854775807B", (1<<63 - 1) * bytesize.Byte}, + {"-9223372036854775808B", -1 << 63 * bytesize.Byte}, + // largest negative value + {"-9223372036854775808B", -1 << 63 * bytesize.Byte}, + {"0.100000000000000000000MiB", bytesize.Mebibyte * 1 / 10}, + + // simple + {"5GB", 5 * bytesize.Gigabyte}, + {"30GB", 30 * bytesize.Gigabyte}, + {"1478GB", 1478 * bytesize.Gigabyte}, + // sign + {"-5GB", -5 * bytesize.Gigabyte}, + {"+5GB", 5 * bytesize.Gigabyte}, + // decimal + {"5.0GB", 5 * bytesize.Gigabyte}, + {"5.6GB", 5*bytesize.Gigabyte + 600*bytesize.Megabyte}, + {"5.GB", 5 * bytesize.Gigabyte}, + {".5GB", 500 * bytesize.Megabyte}, + {"1.0GB", 1 * bytesize.Gigabyte}, + {"1.00GB", 1 * bytesize.Gigabyte}, + {"1.004GB", 1*bytesize.Gigabyte + 4*bytesize.Megabyte}, + {"1.0040GB", 1*bytesize.Gigabyte + 4*bytesize.Megabyte}, + {"100.00100GB", 100*bytesize.Gigabyte + 1*bytesize.Megabyte}, + // different units + {"10B", 10 * bytesize.Byte}, + {"11kB", 11 * bytesize.Kilobyte}, + {"12MB", 12 * bytesize.Megabyte}, + {"12GB", 12 * bytesize.Gigabyte}, + {"13TB", 13 * bytesize.Terabyte}, + {"14PB", 14 * bytesize.Petabyte}, + // composite durations + {"3GB30MB", 3*bytesize.Gigabyte + 30*bytesize.Megabyte}, + {"10.5MB4GB", 4*bytesize.Gigabyte + 10*bytesize.Megabyte + 500*bytesize.Kilobyte}, + {"-2TB3.4GB", -(2*bytesize.Terabyte + 3*bytesize.Gigabyte + 400*bytesize.Megabyte)}, + { + "1PB2TB3GB4MB5kB6B", + 1*bytesize.Petabyte + 2*bytesize.Terabyte + 3*bytesize.Gigabyte + 4*bytesize.Megabyte + 5*bytesize.Kilobyte + 6*bytesize.Byte, + }, + {"39PB9TB14.425GB", 39*bytesize.Petabyte + 9*bytesize.Terabyte + 14*bytesize.Gigabyte + 425*bytesize.Megabyte}, + {"9223372036854775.807kB", (1<<63 - 1) * bytesize.Byte}, + {"9223372036GB854MB775kB807B", (1<<63 - 1) * bytesize.Byte}, + {"-9223372036854775.808kB", -1 << 63 * bytesize.Byte}, + {"-9223372036GB854MB775kB808B", -1 << 63 * bytesize.Byte}, + // largest negative round trip value + {"-9223372036GB854MB775.808kB", -1 << 63 * bytesize.Byte}, +} + +func TestParse(t *testing.T) { + t.Parallel() + + for _, tc := range parseTests { + d, err := bytesize.Parse(tc.in) + if err != nil || d != tc.want { + t.Errorf("Parse(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) + } + } +} + +var parseErrorTests = []struct { + in string + expect string +}{ + // invalid + {"", `""`}, + {"3", `"3"`}, + {"-", `"-"`}, + {"kB", `"kB"`}, + {".", `"."`}, + {"-.", `"-."`}, + {".MB", `".MB"`}, + {"+.MB", `"+.MB"`}, + {"1ExB", `"1ExB"`}, + {"\x85\x85", `"\x85\x85"`}, + {"\xffff", `"\xffff"`}, + {"hello \xffff world", `"hello \xffff world"`}, + {"\uFFFD", `"�"`}, // utf8.RuneError + {"\uFFFD hello \uFFFD world", `"� hello � world"`}, // utf8.RuneError + // overflow + {"9223372036854775810B", `"9223372036854775810B"`}, + {"9223372036854775808B", `"9223372036854775808B"`}, + {"-9223372036854775809B", `"-9223372036854775809B"`}, + {"9223372036854776kB", `"9223372036854776kB"`}, + {"3000000PB", `"3000000PB"`}, + {"9223372036854775.808KiB", `"9223372036854775.808KiB"`}, + {"9223372036854MB775kB808B", `"9223372036854MB775kB808B"`}, +} + +func TestParseErrors(t *testing.T) { + t.Parallel() + + for _, tc := range parseErrorTests { + _, err := bytesize.Parse(tc.in) + if err == nil { + t.Errorf("Parse(%q) = _, nil, want _, non-nil", tc.in) + } else if !strings.Contains(err.Error(), tc.expect) { + t.Errorf("Parse(%q) = _, %q, error does not contain %q", tc.in, err, tc.expect) + } + } +} + +func TestParseRoundTrip(t *testing.T) { + t.Parallel() + + max0 := bytesize.Size(math.MaxInt64) + max1, err := bytesize.Parse(max0.String()) + + if err != nil || max0 != max1 { + t.Errorf("round-trip failed: %d => %q => %d, %v", max0, max0.String(), max1, err) + } + + min0 := bytesize.Size(math.MinInt64) + min1, err := bytesize.Parse(min0.String()) + + if err != nil || min0 != min1 { + t.Errorf("round-trip failed: %d => %q => %d, %v", min0, min0.String(), min1, err) + } + + for range 100 { + // Resolutions finer than milliseconds will result in + // imprecise round-trips. + d0 := bytesize.Size(rand.Int31()) * bytesize.Megabyte + s := d0.String() + d1, err := bytesize.Parse(s) + + if err != nil || d0 != d1 { + t.Errorf("round-trip failed: %d => %q => %d, %v", d0, s, d1, err) + } + } +} diff --git a/size_text.go b/size_text.go new file mode 100644 index 0000000..3f902bf --- /dev/null +++ b/size_text.go @@ -0,0 +1,20 @@ +package bytesize + +import "fmt" + +func (s Size) MarshalText() ([]byte, error) { + val := s.String() + + return []byte(val), nil +} + +func (s *Size) UnmarshalText(text []byte) error { + val, err := Parse(string(text)) + if err != nil { + return fmt.Errorf("%w: unmarshal text", err) + } + + *s = val + + return nil +} diff --git a/size_text_test.go b/size_text_test.go new file mode 100644 index 0000000..176e170 --- /dev/null +++ b/size_text_test.go @@ -0,0 +1,55 @@ +package bytesize_test + +import ( + "encoding/json" + "math" + "testing" + + "gitoa.ru/go-4devs/bytesize" +) + +var marshalTextTests = []struct { + in bytesize.Size + expect string +}{ + {bytesize.Size(0), `"0B"`}, + {bytesize.Size(100), `"100B"`}, + {bytesize.Size(1024), `"1.024kB"`}, + {bytesize.Size(1024 * 1024), `"1.048576MB"`}, + {bytesize.Size(1024 * 1024 * 1024), `"1.073741824GB"`}, + {bytesize.Size(1024 * 1024 * 1024 * 1024), `"1.099511627776TB"`}, + {bytesize.Size(math.MaxInt64), `"9223.372036854775807PB"`}, + {bytesize.Size(1000), `"1kB"`}, + {bytesize.Size(1000 * 1000), `"1MB"`}, + {bytesize.Size(1000 * 1000 * 1000), `"1GB"`}, + {bytesize.Size(1000 * 1000 * 1000 * 1000), `"1TB"`}, + {bytesize.Size(1000 * 1000 * 1000 * 1000 * 1000), `"1PB"`}, +} + +func TestMarshalText(t *testing.T) { + t.Parallel() + + for _, tc := range marshalTextTests { + data, err := json.Marshal(tc.in) + if err != nil { + t.Errorf("json.Marshal(%q) = _, err:%q", tc.in, err) + } else if string(data) != tc.expect { + t.Errorf("json.Marshal(%q) = %q, data does not equals %q", tc.in, data, tc.expect) + } + } +} + +func TestUnmarshalText(t *testing.T) { + t.Parallel() + + for _, tc := range marshalTextTests { + var data bytesize.Size + err := json.Unmarshal([]byte(tc.expect), &data) + + if err != nil { + t.Errorf("json.Unmarshal(%q) = _, err:%q", tc.in, err) + } else if data != tc.in { + t.Errorf("json.Unmarshal(%q) = %q, data does not equals %q", tc.in, data, tc.expect) + } + } +}