diff options
| author | Shulhan <ms@kilabit.info> | 2022-07-22 00:37:20 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-07-22 00:37:20 +0700 |
| commit | 964ec77ee6bd2099a48bdd72c51739a264183538 (patch) | |
| tree | 0c6d564b6c5b208ae06d50e3b9e42103e56d4f32 | |
| parent | 78ab8ecb0be6fab81837b40d208ffdde70865a07 (diff) | |
| download | pakakeh.go-964ec77ee6bd2099a48bdd72c51739a264183538.tar.xz | |
lib/test: implement Data, a type to load formatted file for helping test
Data contains predefined input and output values that is loaded from
file to be used during test.
The data provides zero or more flags, an optional description, zero or
more input, and zero or more output.
The data file name must end with ".txt".
The data content use the following format,
[FLAG_KEY ":" FLAG_VALUE LF]
[LF DESCRIPTION]
">>>" [INPUT_NAME] LF
INPUT_CONTENT
LF
"<<<" [OUTPUT_NAME] LF
OUTPUT_CONTENT
The data can contains zero or more flag.
A flag is key and value separated by ":".
The flag key must not contain spaces.
The data may contain description.
The line that start with "\n>>>" defined the beginning of input.
An input can have a name, if its empty it will be set to "default".
An input can be defined multiple times, with different names.
The line that start with "\n<<<" defined the beginning of output.
An output can have a name, if its empty it will be set to "default".
An output also can be defined multiple times, with different names.
| -rw-r--r-- | lib/test/data.go | 319 | ||||
| -rw-r--r-- | lib/test/data_test.go | 130 | ||||
| -rw-r--r-- | lib/test/example_test.go | 100 | ||||
| -rw-r--r-- | lib/test/testdata/not_loaded | 1 | ||||
| -rw-r--r-- | lib/test/testdata/test1.txt | 7 | ||||
| -rw-r--r-- | lib/test/testdata/test2.txt | 5 |
6 files changed, 562 insertions, 0 deletions
diff --git a/lib/test/data.go b/lib/test/data.go new file mode 100644 index 00000000..319c8ee4 --- /dev/null +++ b/lib/test/data.go @@ -0,0 +1,319 @@ +// Copyright 2022, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package test + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/shuLhan/share/lib/ascii" +) + +const ( + defDataName = "default" + defDataExt = ".txt" +) + +var ( + prefixInput = []byte(">>>") + prefixOutput = []byte("<<<") +) + +// Data contains predefined input and output values that is loaded from +// file to be used during test. +// +// The data provides zero or more flags, an optional description, zero or +// more input, and zero or more output. +// +// The data file name must end with ".txt". +// +// The data content use the following format, +// +// [FLAG_KEY ":" FLAG_VALUE LF] +// [LF DESCRIPTION] +// ">>>" [INPUT_NAME] LF +// INPUT_CONTENT +// LF +// "<<<" [OUTPUT_NAME] LF +// OUTPUT_CONTENT +// +// The data can contains zero or more flag. +// A flag is key and value separated by ":". +// The flag key must not contain spaces. +// +// The data may contain description. +// +// The line that start with "\n>>>" defined the beginning of input. +// An input can have a name, if its empty it will be set to "default". +// An input can be defined multiple times, with different names. +// +// The line that start with "\n<<<" defined the beginning of output. +// An output can have a name, if its empty it will be set to "default". +// An output also can be defined multiple times, with different names. +// +// # Example +// +// The following code illustrate how to use Data when writing test. +// +// Assume that we are writing a parser that consume []byte. +// First we pass the input as defined in ">>>" and then +// we dump the result into bytes.Buffer to be compare with output "<<<". +// +// func TestParse(t *testing.T) { +// var buf bytes.Buffer +// tdata, _ := LoadData("testdata/data.txt") +// opt := tdata.Flag["env"] +// p, err := Parse(tdata.Input["default"], opt) +// if err != nil { +// Assert(t, "Error", tdata.Output["error"], []byte(err.Error()) +// } +// fmt.Fprintf(&buf, "%v", p) +// want := tdata.Output["default"] +// got := buf.Bytes() +// Assert(t, tdata.Name, want, got) +// } +// +// That is the gist, the real application can consume one or more input; or +// generate one or more output. +type Data struct { + Flag map[string]string + Input map[string][]byte + Output map[string][]byte + + // The file name of the data. + Name string + + Desc []byte +} + +// LoadData load data from file. +func LoadData(file string) (data *Data, err error) { + var ( + logp = "LoadData" + + content []byte + ) + + content, err = os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + data = newData(filepath.Base(file)) + + err = data.parse(content) + if err != nil { + return nil, err + } + + return data, nil +} + +// LoadDataDir load all data inside a directory. +// Only file that has ".txt" extension will be loaded. +func LoadDataDir(path string) (listData []*Data, err error) { + var ( + logp = "LoadDataDir" + + dir *os.File + listfi []os.FileInfo + fi os.FileInfo + data *Data + name string + ext string + pathData string + ) + + dir, err = os.Open(path) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + listfi, err = dir.Readdir(0) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + for _, fi = range listfi { + if fi.Size() == 0 { + continue + } + + name = fi.Name() + + ext = filepath.Ext(name) + ext = strings.ToLower(ext) + if ext != defDataExt { + continue + } + + pathData = filepath.Join(path, name) + + data, err = LoadData(pathData) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + listData = append(listData, data) + } + return listData, nil +} + +func isFlag(content []byte) bool { + var c byte + for _, c = range content { + if ascii.IsSpace(c) { + return false + } + if c == ':' { + return true + } + } + return false +} + +func newData(name string) (data *Data) { + data = &Data{ + Name: name, + Flag: make(map[string]string), + Input: make(map[string][]byte), + Output: make(map[string][]byte), + } + return data +} + +func (data *Data) parse(content []byte) (err error) { + const ( + stateFlag int = iota + stateDesc + stateInputOutput + stateInput + stateOutput + ) + + var ( + logp = "LoadData" + + name string + lines [][]byte + state int + n int + x int + ) + + lines = bytes.Split(content, []byte("\n")) + + for x < len(lines) { + content = lines[x] + if state == stateFlag { + if len(content) == 0 { + x++ + continue + } + if isFlag(content) { + data.parseFlag(content) + x++ + continue + } + state = stateDesc + } + if state == stateDesc { + if len(content) == 0 { + x++ + continue + } + if !(bytes.HasPrefix(content, prefixInput) || bytes.HasPrefix(content, prefixOutput)) { + if len(data.Desc) > 0 { + data.Desc = append(data.Desc, '\n') + } + data.Desc = append(data.Desc, content...) + x++ + continue + } + state = stateInputOutput + } + if bytes.HasPrefix(content, prefixInput) { + name, content, n = data.parseInputOutput(lines[x:]) + data.Input[name] = content + x += n + continue + } + if bytes.HasPrefix(content, prefixOutput) { + name, content, n = data.parseInputOutput(lines[x:]) + data.Output[name] = content + x += n + continue + } + return fmt.Errorf("%s: unknown syntax line %d: %s", logp, x, content) + } + return nil +} + +func (data *Data) parseFlag(content []byte) { + var ( + idx int = bytes.IndexByte(content, ':') + bkey []byte + bval []byte + ) + if idx < 0 { + return + } + + bkey = bytes.TrimSpace(content[:idx]) + if len(bkey) == 0 { + return + } + + bval = bytes.TrimSpace(content[idx+1:]) + + data.Flag[string(bkey)] = string(bval) +} + +func (data *Data) parseInputOutput(lines [][]byte) (name string, content []byte, n int) { + var ( + line []byte + bname []byte + bufContent bytes.Buffer + x int + isPrevEmpty bool + ) + + line = lines[0] + bname = bytes.TrimSpace(line[3:]) + if len(bname) == 0 { + name = defDataName + } else { + name = string(bname) + } + + for x = 1; x < len(lines); x++ { + line = lines[x] + if len(line) == 0 { + if isPrevEmpty { + bufContent.WriteByte('\n') + } else { + isPrevEmpty = true + } + continue + } + if isPrevEmpty { + if bytes.HasPrefix(line, prefixInput) || bytes.HasPrefix(line, prefixOutput) { + content = bufContent.Bytes() + return name, content, x + } + bufContent.WriteByte('\n') + } + bufContent.Write(line) + bufContent.WriteByte('\n') + isPrevEmpty = false + } + + content = bufContent.Bytes() + + return name, content, x +} diff --git a/lib/test/data_test.go b/lib/test/data_test.go new file mode 100644 index 00000000..b6a9f3f0 --- /dev/null +++ b/lib/test/data_test.go @@ -0,0 +1,130 @@ +// Copyright 2022, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package test + +import "testing" + +func TestData_parse(t *testing.T) { + type testCase struct { + desc string + content []byte + expData Data + } + + var cases = []testCase{{ + desc: `With flag only`, + content: []byte("\na: b\nc: d\n"), + expData: Data{ + Flag: map[string]string{ + `a`: `b`, + `c`: `d`, + }, + }, + }, { + desc: `With description only`, + content: []byte("\nDesc."), + expData: Data{ + Desc: []byte("Desc."), + }, + }, { + desc: `With input only`, + content: []byte(">>>\n\ninput.\n\n"), + expData: Data{ + Input: map[string][]byte{ + `default`: []byte("\ninput.\n\n"), + }, + }, + }, { + desc: `With output only`, + content: []byte("<<<\n\noutput.\n\n"), + expData: Data{ + Output: map[string][]byte{ + `default`: []byte("\noutput.\n\n"), + }, + }, + }, { + desc: `With flag and description`, + content: []byte("a: b\nMulti\nline\ndescription.\n"), + expData: Data{ + Flag: map[string]string{ + `a`: `b`, + }, + Desc: []byte("Multi\nline\ndescription."), + }, + }, { + desc: `With multi input`, + content: []byte("a: b\n" + + "Desc.\n" + + ">>> input 1\n1\n\n" + + ">>> input 2\n2\n", + ), + expData: Data{ + Flag: map[string]string{ + `a`: `b`, + }, + Desc: []byte("Desc."), + Input: map[string][]byte{ + "input 1": []byte("1\n"), + "input 2": []byte("2\n"), + }, + }, + }, { + desc: `With multi output`, + content: []byte("Desc.\n" + + "<<< output-1\n1\n\n2\n\n" + + "<<< output-2\n3\n\n4\n", + ), + expData: Data{ + Flag: map[string]string{}, + Desc: []byte("Desc."), + Output: map[string][]byte{ + "output-1": []byte("1\n\n2\n"), + "output-2": []byte("3\n\n4\n"), + }, + }, + }, { + desc: `With input duplicate names`, + content: []byte(">>>\n" + + "input 1\n\n" + + ">>> default\nInput 2.\n", + ), + expData: Data{ + Flag: map[string]string{}, + Input: map[string][]byte{ + "default": []byte("Input 2.\n"), + }, + }, + }, { + desc: `With no newline above output`, + content: []byte(">>>\n" + + "Input 1.\n" + + "<<<\n" + + "Output 1.\n", + ), + expData: Data{ + Input: map[string][]byte{ + "default": []byte("Input 1.\n<<<\nOutput 1.\n"), + }, + }, + }} + + var ( + c testCase + gotData *Data + err error + ) + + for _, c = range cases { + t.Run(c.desc, func(t *testing.T) { + gotData = newData("") + err = gotData.parse(c.content) + if err != nil { + t.Fatal(err) + } + + Assert(t, "Data", &c.expData, gotData) + }) + } +} diff --git a/lib/test/example_test.go b/lib/test/example_test.go new file mode 100644 index 00000000..baa63195 --- /dev/null +++ b/lib/test/example_test.go @@ -0,0 +1,100 @@ +// Copyright 2022, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package test + +import ( + "fmt" + "log" +) + +func ExampleLoadDataDir() { + var ( + listData []*Data + data *Data + err error + name string + content []byte + ) + + listData, err = LoadDataDir("testdata/") + if err != nil { + log.Fatal(err) + } + + for _, data = range listData { + fmt.Printf("%s\n", data.Name) + fmt.Printf(" Flags=%v\n", data.Flag) + fmt.Printf(" Desc=%s\n", data.Desc) + fmt.Println(" Input") + for name, content = range data.Input { + fmt.Printf(" %s=%s", name, content) + } + fmt.Println(" Output") + for name, content = range data.Output { + fmt.Printf(" %s=%s", name, content) + } + } + + // Output: + // test2.txt + // Flags=map[] + // Desc= + // Input + // default=another test input. + // Output + // default=another test output. + // test1.txt + // Flags=map[key:value] + // Desc=Description of test1. + // Input + // default=input. + // Output + // default=output. +} + +func ExampleLoadData() { + var ( + data *Data + name string + content []byte + err error + ) + + // Content of test1.txt, + // + // key: value + // Description of test1. + // >>> + // input. + // + // <<< + // output. + + data, err = LoadData("testdata/test1.txt") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", data.Name) + fmt.Printf(" Flags=%v\n", data.Flag) + fmt.Printf(" Desc=%s\n", data.Desc) + fmt.Println(" Input") + for name, content = range data.Input { + fmt.Printf(" %s=%s", name, content) + } + fmt.Println(" Output") + for name, content = range data.Output { + fmt.Printf(" %s=%s", name, content) + } + + // Output: + // test1.txt + // Flags=map[key:value] + // Desc=Description of test1. + // Input + // default=input. + // Output + // default=output. +} diff --git a/lib/test/testdata/not_loaded b/lib/test/testdata/not_loaded new file mode 100644 index 00000000..327e1d0b --- /dev/null +++ b/lib/test/testdata/not_loaded @@ -0,0 +1 @@ +Not loaded. diff --git a/lib/test/testdata/test1.txt b/lib/test/testdata/test1.txt new file mode 100644 index 00000000..323ab56f --- /dev/null +++ b/lib/test/testdata/test1.txt @@ -0,0 +1,7 @@ +key: value +Description of test1. +>>> +input. + +<<< +output. diff --git a/lib/test/testdata/test2.txt b/lib/test/testdata/test2.txt new file mode 100644 index 00000000..9a1ebdcc --- /dev/null +++ b/lib/test/testdata/test2.txt @@ -0,0 +1,5 @@ +>>> +another test input. + +<<< +another test output. |
