summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-07-22 00:37:20 +0700
committerShulhan <ms@kilabit.info>2022-07-22 00:37:20 +0700
commit964ec77ee6bd2099a48bdd72c51739a264183538 (patch)
tree0c6d564b6c5b208ae06d50e3b9e42103e56d4f32
parent78ab8ecb0be6fab81837b40d208ffdde70865a07 (diff)
downloadpakakeh.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.go319
-rw-r--r--lib/test/data_test.go130
-rw-r--r--lib/test/example_test.go100
-rw-r--r--lib/test/testdata/not_loaded1
-rw-r--r--lib/test/testdata/test1.txt7
-rw-r--r--lib/test/testdata/test2.txt5
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.