From 41043c6e830fe86aacd37d44e0bad3dd6d1ac895 Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Tue, 2 Jun 2026 18:20:12 +0530 Subject: [PATCH] feat(tests): add test for analyzer.go Signed-off-by: Priyansh Sao --- challenge-01/analyzer/analyzer.go | 30 ++-- challenge-01/analyzer/analyzer_test.go | 212 +++++++++++++++++++++++++ challenge-01/cmd/ccwc.go | 4 +- challenge-01/config/config.go | 3 +- 4 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 challenge-01/analyzer/analyzer_test.go diff --git a/challenge-01/analyzer/analyzer.go b/challenge-01/analyzer/analyzer.go index 8679346..a8c00c3 100644 --- a/challenge-01/analyzer/analyzer.go +++ b/challenge-01/analyzer/analyzer.go @@ -5,11 +5,11 @@ import ( "io" "unicode/utf8" - stdconfig "github.com/priyanshsao/coding-challenges-go/challenge-01/config" + "github.com/priyanshsao/coding-challenges-go/challenge-01/config" ) // Process computes the value of each option. -func Process(c *stdconfig.Config) error { +func Process(c *config.Config) error { buffer := make([]byte, 32*1024) //32kB leftOver := []byte{} @@ -30,13 +30,13 @@ func Process(c *stdconfig.Config) error { if val { switch opts { - case stdconfig.BYTES: + case config.BYTES: processBytes(buffer[:n], &c.Result) - case stdconfig.LINES: + case config.LINES: processLines(buffer[:n], &c.Result) - case stdconfig.WORDS: + case config.WORDS: processWords(buffer[:n], &c.Result, &inWord) - case stdconfig.RUNES: + case config.RUNES: processRunes(buffer[:n], &c.Result, &leftOver) } } @@ -46,19 +46,19 @@ func Process(c *stdconfig.Config) error { return nil } -func processBytes(buffer []byte, result *stdconfig.Result) { +func processBytes(buffer []byte, result *config.Result) { if len(buffer) > 0 { - (*result)[stdconfig.BYTES] += len(buffer) + (*result)[config.BYTES] += len(buffer) } } -func processLines(buffer []byte, result *stdconfig.Result) { +func processLines(buffer []byte, result *config.Result) { - (*result)[stdconfig.LINES] += bytes.Count(buffer, []byte{'\n'}) + (*result)[config.LINES] += bytes.Count(buffer, []byte{'\n'}) } -func processWords(buffer []byte, result *stdconfig.Result, inWord *bool) { +func processWords(buffer []byte, result *config.Result, inWord *bool) { for _, b := range buffer { if isSpace(b) { @@ -66,14 +66,14 @@ func processWords(buffer []byte, result *stdconfig.Result, inWord *bool) { } else { if !*inWord { - (*result)[stdconfig.WORDS]++ + (*result)[config.WORDS]++ *inWord = true } } } } -func processRunes(buffer []byte, result *stdconfig.Result, leftOver *[]byte) { +func processRunes(buffer []byte, result *config.Result, leftOver *[]byte) { buffer = append(*leftOver, buffer...) *leftOver = (*leftOver)[:0] @@ -88,7 +88,7 @@ func processRunes(buffer []byte, result *stdconfig.Result, leftOver *[]byte) { // as we are sure there is atleast 1 rune ahed _, size := utf8.DecodeRune(buffer[i:]) - (*result)[stdconfig.RUNES]++ + (*result)[config.RUNES]++ i += size } } @@ -96,4 +96,4 @@ func processRunes(buffer []byte, result *stdconfig.Result, leftOver *[]byte) { func isSpace(b byte) bool { // simple ASCII check return b == ' ' || b == '\n' || b == '\t' || b == '\r' -} \ No newline at end of file +} diff --git a/challenge-01/analyzer/analyzer_test.go b/challenge-01/analyzer/analyzer_test.go new file mode 100644 index 0000000..32b909d --- /dev/null +++ b/challenge-01/analyzer/analyzer_test.go @@ -0,0 +1,212 @@ +package analyzer + +import ( + "reflect" + "testing" + + "github.com/priyanshsao/coding-challenges-go/challenge-01/config" +) + +func Test_ProcessBytes(t *testing.T) { + + tests := []struct { + name string + input string + expected int + }{ + {"empty", "", 0}, + {"ascii", "hello", 5}, + + // edge cases + {"escape sequence", "\n\n\t\r", 4}, + {"unicode", "😍😍😍", 12}, //😍 = 4 bytes + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + result := config.New().Result + + processBytes([]byte(tt.input), &result) + + if result[config.BYTES] != tt.expected { + t.Errorf("expected %d but got %d", tt.expected, result[config.BYTES]) + } + }) + } +} + +func Test_ProcessLines(t *testing.T) { + + // Todo: currently processLine only + // counts \n in future we need to add + // logic and test for counting lines for files + // that dont end with '\n' or '\r\n'. + + tests := []struct { + name string + input string + expected int + }{ + {"line feed", "Hi,\n I am a dev", 1}, + {"CR+LF", "Hi,\r\n I am a dev\r\n", 2}, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + result := config.New().Result + + processLines([]byte(tt.input), &result) + + if result[config.LINES] != tt.expected { + t.Errorf("expected %d but got %d", tt.expected, result[config.LINES]) + } + }) + } +} + +func Test_ProcessWords(t *testing.T) { + + tests := []struct { + name string + input []string + expected int + expectedState []bool + }{ + // empty []string{} case + // is not present, because + // function's loop doesn't run for that case, + // so we think there is no need to + // test that. + { + "empty string", + []string{""}, + 0, + []bool{false}, + }, + { + "normally spaced words", + []string{"Hi ", "I am", " a developer"}, + 5, + []bool{false, true, true}, + }, + + // edge cases + { + "seperated by escape characters", + []string{"Hi,\tI\tam\ta\ndeveloper"}, + 5, + []bool{true}, + }, + { + "rune input", + []string{"😍", "😍", "😍"}, + 1, + []bool{true, true, true}, + }, + { + "incomplete words across buffer", + []string{" h", "ey! this i", "s cc", "wc "}, + 4, + []bool{true, true, true, false}, + }, + { + "single character in each buffer", + []string{"h", "e", "l", "l", "o"}, + 1, + []bool{true, true, true, true, true}, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + inWord := new(bool) + result := config.New().Result + + for i, str := range tt.input { + processWords([]byte(str), &result, inWord) + + if *inWord != tt.expectedState[i] { + t.Errorf("expected %v but got %v", tt.expectedState[i], *inWord) + } + } + + if result[config.WORDS] != tt.expected { + t.Errorf("expected %v but got this %v", tt.expected, result[config.WORDS]) + } + }) + } +} + +func Test_ProcessRunes(t *testing.T) { + + r := []byte("😍") + + // This is an invalid utf-8 character + invalidUTF8 := []byte{0xFF} + + type step struct { + input []byte + leftOver []byte + } + + tests := []struct { + name string + steps []step + expected int + }{ + { + name: "incomplete rune", + steps: []step{ + {input: r[:2], leftOver: r[:2]}, + {input: r[2:], leftOver: []byte{}}, + }, + expected: 1, + }, + { + name: "ascii characters", + steps: []step{ + {input: []byte("abcd"), leftOver: nil}, + }, + expected: 4, + }, + { + name: "incomplete end", + steps: []step{ + {input: r[:2], leftOver: r[:2]}, + }, + expected: 0, + }, + { + name: "invalid utf-8", + steps: []step{ + {input: invalidUTF8, leftOver: nil}, + }, + expected: 1, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + var leftOver []byte + result := config.New().Result + + for _, s := range tt.steps { + + processRunes(s.input, &result, &leftOver) + + if !reflect.DeepEqual(leftOver, s.leftOver) { + t.Errorf("expected %v but got %v", s.leftOver, leftOver) + } + } + + if result[config.RUNES] != tt.expected { + t.Errorf("expected %v but got %v", tt.expected, result[config.RUNES]) + } + }) + } +} diff --git a/challenge-01/cmd/ccwc.go b/challenge-01/cmd/ccwc.go index d49add5..d51a7d7 100644 --- a/challenge-01/cmd/ccwc.go +++ b/challenge-01/cmd/ccwc.go @@ -54,13 +54,11 @@ func main() { } if err := analyzer.Read(c); err != nil { - // Todo: log error logrus.Errorf("unable to read input: %v", err) return } if err := analyzer.Process(c); err != nil { - // Todo: log error logrus.Errorf("unable to process input: %v", err) return } @@ -72,7 +70,7 @@ func setLogger() { // remove unwanted things and enforce colors logrus.SetFormatter(&logrus.TextFormatter{ - ForceColors: true, + ForceColors: true, DisableLevelTruncation: true, }) } diff --git a/challenge-01/config/config.go b/challenge-01/config/config.go index 0bc474e..ee6aae2 100644 --- a/challenge-01/config/config.go +++ b/challenge-01/config/config.go @@ -40,7 +40,7 @@ type Config struct { Result Result } -// New returns a new config for analyzing input. +// New returns a new config for analyzing input. func New() *Config { config := &Config{ @@ -51,7 +51,6 @@ func New() *Config { return config } -// String value of an operation. func (opts FileOpts) String() string { var str string