github.com/KEINOS/go-countline@v1.1.1-0.20221217083629-60710df7606b/cl/spec/spec.go (about)

     1  /*
     2  Package spec provides the test specifications for the CountLines function.
     3  
     4  Alternate implementations of CountLines function must pass the test as well.
     5  */
     6  package spec
     7  
     8  import (
     9  	"bufio"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/zenizh/go-capturer"
    18  )
    19  
    20  // ============================================================================
    21  //  Data Provider of CountLines Specification
    22  // ============================================================================
    23  
    24  // DataCountLines is the data provider for the CountLines function to check if
    25  // the specifications are covered.
    26  // Alternate functions must pass the test with this data as well.
    27  //
    28  //nolint:gomnd,gochecknoglobals // numbers of ExpectOut are not magic numbers and let DataCountLines be global.
    29  var DataCountLines = []struct {
    30  	Reason    string // Reason on failure
    31  	Input     string // Input data
    32  	ExpectOut int    // Expected output
    33  }{
    34  	{
    35  		Reason:    "'<EOF>' --> empty file should be zero",
    36  		Input:     "",
    37  		ExpectOut: 0,
    38  	},
    39  	{
    40  		Reason:    "'Hello<EOF>' --> single line without line break should be one",
    41  		Input:     "Hello",
    42  		ExpectOut: 1,
    43  	},
    44  	{
    45  		Reason:    "'Hello\\n<EOF>' --> single line with line break should be one",
    46  		Input:     "Hello\n",
    47  		ExpectOut: 1,
    48  	},
    49  	{
    50  		Reason:    "'\\n<EOF>' --> single line break should be one",
    51  		Input:     "\n",
    52  		ExpectOut: 1,
    53  	},
    54  	{
    55  		Reason:    "'\\n\\n<EOF>' --> two line breaks should be two",
    56  		Input:     "\n\n",
    57  		ExpectOut: 2,
    58  	},
    59  	{
    60  		Reason:    "'\\nHello<EOF>' --> one line break and one line without line break should be two",
    61  		Input:     "\nHello",
    62  		ExpectOut: 2,
    63  	},
    64  	{
    65  		Reason:    "'\\nHello\\n<EOF>' --> one line break and one line with line break should be two",
    66  		Input:     "\nHello\n",
    67  		ExpectOut: 2,
    68  	},
    69  	{
    70  		Reason:    "'\\n\\nHello<EOF>' --> two line breaks and one line without line break should be three",
    71  		Input:     "\n\nHello",
    72  		ExpectOut: 3,
    73  	},
    74  	{
    75  		Reason:    "'\\n\\nHello\\n<EOF>' --> two line breaks and one line with line break should be three",
    76  		Input:     "\n\nHello\n",
    77  		ExpectOut: 3,
    78  	},
    79  	{
    80  		Reason:    "'<large line>\\n<EOF>' --> long string with a line break should be onw",
    81  		Input:     GetStrDummyLines(bufio.MaxScanTokenSize*2, 1),
    82  		ExpectOut: 1,
    83  	},
    84  	{
    85  		Reason:    "'<large line>\\n<large line>\\n<EOF>' --> long string but two lines should be two",
    86  		Input:     GetStrDummyLines(bufio.MaxScanTokenSize*2, 2),
    87  		ExpectOut: 2,
    88  	},
    89  }
    90  
    91  // ============================================================================
    92  //  Functions
    93  // ============================================================================
    94  
    95  // ----------------------------------------------------------------------------
    96  //  RunSpecTest
    97  // ----------------------------------------------------------------------------
    98  
    99  // RunSpecTest is a helper function to run the specifcations of LineCount function.
   100  // Alternate implementations (_alt.*) must pass this test as well.
   101  //
   102  //nolint:varnamelen // fn is short for the scope of its usage but leave it as is.
   103  func RunSpecTest(t *testing.T, nameFn string, fn func(io.Reader) (int, error)) {
   104  	t.Helper()
   105  
   106  	const threshold = 1024 // Max size of input data to begin cropping
   107  
   108  	for index, test := range DataCountLines {
   109  		testNum := fmt.Sprintf("test #%v", index)
   110  
   111  		t.Run(testNum, func(t *testing.T) {
   112  			logInput := fmt.Sprintf("%v", test.Input)
   113  
   114  			// Crop the input to make it readable
   115  			if len(test.Input) > threshold {
   116  				logInput = fmt.Sprintf("%v", test.Input[:64]+"..."+test.Input[len(test.Input)-64:])
   117  			}
   118  
   119  			t.Logf("Input : %#v", logInput)
   120  			t.Log("Input len:", len(test.Input))
   121  
   122  			ioReader := strings.NewReader(test.Input)
   123  
   124  			msgReason := fmt.Sprintf("%v %v: %v", nameFn, testNum, test.Reason)
   125  			expect := test.ExpectOut
   126  
   127  			// Run the target function. Capture the STDOUT and STDERR as well.
   128  			out := capturer.CaptureOutput(func() {
   129  				actual, err := fn(ioReader)
   130  
   131  				require.NoError(t, err, "golden case should not return error")
   132  				assert.Equal(t, expect, actual, msgReason)
   133  			})
   134  
   135  			assert.Emptyf(t, out,
   136  				"%v %v: it should not output to STDOUT/STDERR on success.\nOut: %v",
   137  				nameFn, testNum, out,
   138  			)
   139  		})
   140  	}
   141  }
   142  
   143  // ----------------------------------------------------------------------------
   144  //  GetStrDummyLines
   145  // ----------------------------------------------------------------------------
   146  
   147  // GetStrDummyLines returns a string with the given size and number of lines.
   148  // 'sizeLine' is the size of each line. 'numLines' is the number of lines.
   149  func GetStrDummyLines(sizeLine, numLine int64) string {
   150  	dataLine := genOneLine(sizeLine)
   151  
   152  	result := ""
   153  	for i := int64(0); i < numLine; i++ {
   154  		result += string(dataLine)
   155  	}
   156  
   157  	return result
   158  }
   159  
   160  // ----------------------------------------------------------------------------
   161  //  genOneLine
   162  // ----------------------------------------------------------------------------
   163  
   164  func genOneLine(sizeLine int64) []byte {
   165  	if sizeLine <= 0 {
   166  		return []byte{}
   167  	}
   168  
   169  	const LF = byte(0x0a) //nolint:varnamelen
   170  
   171  	dataLine := make([]byte, sizeLine)
   172  
   173  	for i := int64(0); i < sizeLine; i++ {
   174  		dataLine[i] = 'a'
   175  
   176  		if i == sizeLine-1 {
   177  			dataLine[i] = LF // \n
   178  		}
   179  	}
   180  
   181  	return dataLine
   182  }