From 1b17dca4349fa6209ae163ded0437f4bb272e90a Mon Sep 17 00:00:00 2001 From: Inanc Gumus Date: Fri, 10 May 2019 22:08:17 +0300 Subject: [PATCH] add: method logparser tests --- .../log.txt | 0 .../log_err_missing.txt | 0 .../log_err_negative.txt | 0 .../log_err_str.txt | 0 .../main.go | 0 .../parser.go | 0 .../result.go | 0 .../summarize.go | 0 .../summary.go | 0 .../{xxx-log-parser => logparser-pkg}/log.txt | 0 .../log_err_missing.txt | 0 .../log_err_negative.txt | 0 .../log_err_str.txt | 0 .../{xxx-log-parser => logparser-pkg}/main.go | 2 +- .../report/parser.go | 0 .../report/result.go | 0 .../report/summary.go | 0 .../summarize.go | 2 +- 28-methods/logparser-testing/log.txt | 6 ++ .../logparser-testing/log_err_missing.txt | 6 ++ .../logparser-testing/log_err_negative.txt | 6 ++ 28-methods/logparser-testing/log_err_str.txt | 6 ++ 28-methods/logparser-testing/main.go | 26 ++++++ 28-methods/logparser-testing/main_test.go | 65 ++++++++++++++ 28-methods/logparser-testing/report/parser.go | 52 +++++++++++ .../logparser-testing/report/parser_test.go | 55 ++++++++++++ 28-methods/logparser-testing/report/result.go | 60 +++++++++++++ .../logparser-testing/report/summary.go | 86 +++++++++++++++++++ .../logparser-testing/report/summary_test.go | 44 ++++++++++ 28-methods/logparser-testing/summarize.go | 73 ++++++++++++++++ 30 files changed, 487 insertions(+), 2 deletions(-) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/log.txt (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/log_err_missing.txt (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/log_err_negative.txt (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/log_err_str.txt (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/main.go (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/parser.go (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/result.go (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/summarize.go (100%) rename 28-methods/{xxx-log-parser-no-pkg => logparser-nopkg}/summary.go (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/log.txt (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/log_err_missing.txt (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/log_err_negative.txt (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/log_err_str.txt (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/main.go (85%) rename 28-methods/{xxx-log-parser => logparser-pkg}/report/parser.go (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/report/result.go (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/report/summary.go (100%) rename 28-methods/{xxx-log-parser => logparser-pkg}/summarize.go (95%) create mode 100644 28-methods/logparser-testing/log.txt create mode 100644 28-methods/logparser-testing/log_err_missing.txt create mode 100644 28-methods/logparser-testing/log_err_negative.txt create mode 100644 28-methods/logparser-testing/log_err_str.txt create mode 100644 28-methods/logparser-testing/main.go create mode 100644 28-methods/logparser-testing/main_test.go create mode 100644 28-methods/logparser-testing/report/parser.go create mode 100644 28-methods/logparser-testing/report/parser_test.go create mode 100644 28-methods/logparser-testing/report/result.go create mode 100644 28-methods/logparser-testing/report/summary.go create mode 100644 28-methods/logparser-testing/report/summary_test.go create mode 100644 28-methods/logparser-testing/summarize.go diff --git a/28-methods/xxx-log-parser-no-pkg/log.txt b/28-methods/logparser-nopkg/log.txt similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/log.txt rename to 28-methods/logparser-nopkg/log.txt diff --git a/28-methods/xxx-log-parser-no-pkg/log_err_missing.txt b/28-methods/logparser-nopkg/log_err_missing.txt similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/log_err_missing.txt rename to 28-methods/logparser-nopkg/log_err_missing.txt diff --git a/28-methods/xxx-log-parser-no-pkg/log_err_negative.txt b/28-methods/logparser-nopkg/log_err_negative.txt similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/log_err_negative.txt rename to 28-methods/logparser-nopkg/log_err_negative.txt diff --git a/28-methods/xxx-log-parser-no-pkg/log_err_str.txt b/28-methods/logparser-nopkg/log_err_str.txt similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/log_err_str.txt rename to 28-methods/logparser-nopkg/log_err_str.txt diff --git a/28-methods/xxx-log-parser-no-pkg/main.go b/28-methods/logparser-nopkg/main.go similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/main.go rename to 28-methods/logparser-nopkg/main.go diff --git a/28-methods/xxx-log-parser-no-pkg/parser.go b/28-methods/logparser-nopkg/parser.go similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/parser.go rename to 28-methods/logparser-nopkg/parser.go diff --git a/28-methods/xxx-log-parser-no-pkg/result.go b/28-methods/logparser-nopkg/result.go similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/result.go rename to 28-methods/logparser-nopkg/result.go diff --git a/28-methods/xxx-log-parser-no-pkg/summarize.go b/28-methods/logparser-nopkg/summarize.go similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/summarize.go rename to 28-methods/logparser-nopkg/summarize.go diff --git a/28-methods/xxx-log-parser-no-pkg/summary.go b/28-methods/logparser-nopkg/summary.go similarity index 100% rename from 28-methods/xxx-log-parser-no-pkg/summary.go rename to 28-methods/logparser-nopkg/summary.go diff --git a/28-methods/xxx-log-parser/log.txt b/28-methods/logparser-pkg/log.txt similarity index 100% rename from 28-methods/xxx-log-parser/log.txt rename to 28-methods/logparser-pkg/log.txt diff --git a/28-methods/xxx-log-parser/log_err_missing.txt b/28-methods/logparser-pkg/log_err_missing.txt similarity index 100% rename from 28-methods/xxx-log-parser/log_err_missing.txt rename to 28-methods/logparser-pkg/log_err_missing.txt diff --git a/28-methods/xxx-log-parser/log_err_negative.txt b/28-methods/logparser-pkg/log_err_negative.txt similarity index 100% rename from 28-methods/xxx-log-parser/log_err_negative.txt rename to 28-methods/logparser-pkg/log_err_negative.txt diff --git a/28-methods/xxx-log-parser/log_err_str.txt b/28-methods/logparser-pkg/log_err_str.txt similarity index 100% rename from 28-methods/xxx-log-parser/log_err_str.txt rename to 28-methods/logparser-pkg/log_err_str.txt diff --git a/28-methods/xxx-log-parser/main.go b/28-methods/logparser-pkg/main.go similarity index 85% rename from 28-methods/xxx-log-parser/main.go rename to 28-methods/logparser-pkg/main.go index b2d6c64..0704485 100644 --- a/28-methods/xxx-log-parser/main.go +++ b/28-methods/logparser-pkg/main.go @@ -11,7 +11,7 @@ import ( "bufio" "os" - "github.com/inancgumus/learngo/28-methods/xxx-log-parser/report" + "github.com/inancgumus/learngo/28-methods/logparser-pkg/report" ) func main() { diff --git a/28-methods/xxx-log-parser/report/parser.go b/28-methods/logparser-pkg/report/parser.go similarity index 100% rename from 28-methods/xxx-log-parser/report/parser.go rename to 28-methods/logparser-pkg/report/parser.go diff --git a/28-methods/xxx-log-parser/report/result.go b/28-methods/logparser-pkg/report/result.go similarity index 100% rename from 28-methods/xxx-log-parser/report/result.go rename to 28-methods/logparser-pkg/report/result.go diff --git a/28-methods/xxx-log-parser/report/summary.go b/28-methods/logparser-pkg/report/summary.go similarity index 100% rename from 28-methods/xxx-log-parser/report/summary.go rename to 28-methods/logparser-pkg/report/summary.go diff --git a/28-methods/xxx-log-parser/summarize.go b/28-methods/logparser-pkg/summarize.go similarity index 95% rename from 28-methods/xxx-log-parser/summarize.go rename to 28-methods/logparser-pkg/summarize.go index 1d9b4ee..c60c89a 100644 --- a/28-methods/xxx-log-parser/summarize.go +++ b/28-methods/logparser-pkg/summarize.go @@ -13,7 +13,7 @@ import ( "os" "strings" - "github.com/inancgumus/learngo/28-methods/xxx-log-parser/report" + "github.com/inancgumus/learngo/28-methods/logparser-pkg/report" ) // summarize prints the parsing results. diff --git a/28-methods/logparser-testing/log.txt b/28-methods/logparser-testing/log.txt new file mode 100644 index 0000000..b449a83 --- /dev/null +++ b/28-methods/logparser-testing/log.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 300 +golang.org 4 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/28-methods/logparser-testing/log_err_missing.txt b/28-methods/logparser-testing/log_err_missing.txt new file mode 100644 index 0000000..8fbe528 --- /dev/null +++ b/28-methods/logparser-testing/log_err_missing.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 +golang.org 4 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/28-methods/logparser-testing/log_err_negative.txt b/28-methods/logparser-testing/log_err_negative.txt new file mode 100644 index 0000000..b099716 --- /dev/null +++ b/28-methods/logparser-testing/log_err_negative.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 300 +golang.org -100 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/28-methods/logparser-testing/log_err_str.txt b/28-methods/logparser-testing/log_err_str.txt new file mode 100644 index 0000000..4ccb676 --- /dev/null +++ b/28-methods/logparser-testing/log_err_str.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 THREE-HUNDRED +golang.org FOUR 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/28-methods/logparser-testing/main.go b/28-methods/logparser-testing/main.go new file mode 100644 index 0000000..eca45b7 --- /dev/null +++ b/28-methods/logparser-testing/main.go @@ -0,0 +1,26 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "os" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +func main() { + p := report.New() + + in := bufio.NewScanner(os.Stdin) + for in.Scan() { + p.Parse(in.Text()) + } + + summarize(p.Summarize(), p.Err(), in.Err()) +} diff --git a/28-methods/logparser-testing/main_test.go b/28-methods/logparser-testing/main_test.go new file mode 100644 index 0000000..b679322 --- /dev/null +++ b/28-methods/logparser-testing/main_test.go @@ -0,0 +1,65 @@ +// +build integration + +// go test -tags=integration + +package main_test + +import ( + "bytes" + "os/exec" + "strings" + "testing" +) + +const ( + okIn = ` +a.com 1 2 +b.com 3 4 +a.com 4 5 +b.com 6 7` + + okOut = ` +DOMAIN VISITS TIME SPENT +----------------------------------------------------------------- +a.com 5 7 +b.com 9 11 + +TOTAL 14 18` +) + +func TestSummary(t *testing.T) { + tests := []struct { + name, in, out string + }{ + {"valid input", okIn, okOut}, + {"missing fields", "a.com 1 2\nb.com 3", "> Err: line #2: missing fields: [b.com 3]"}, + {"incorrect visits", "a.com 1 2\nb.com -1 1", `> Err: line #2: incorrect visits: "-1"`}, + {"incorrect time spent", "a.com 1 2\nb.com 3 -1", `> Err: line #2: incorrect time spent: "-1"`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + run(t, strings.TrimSpace(tt.in), strings.TrimSpace(tt.out)) + }) + } +} + +func run(t *testing.T, in, out string) { + cmd := exec.Command("go", "run", ".") + + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdin.Write([]byte(in)) + stdin.Close() + + got, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, []byte(out+"\n")) { + t.Fatalf("\nwant:\n%s\n\ngot:\n%s", out, got) + } +} diff --git a/28-methods/logparser-testing/report/parser.go b/28-methods/logparser-testing/report/parser.go new file mode 100644 index 0000000..59b12fe --- /dev/null +++ b/28-methods/logparser-testing/report/parser.go @@ -0,0 +1,52 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "fmt" +) + +// Parser parses the log file and generates a summary report. +type Parser struct { + summary *Summary // summarizes the parsing results + lines int // number of parsed lines (for the error messages) + lerr error // the last error occurred +} + +// New returns a new parsing state. +func New() *Parser { + return &Parser{summary: newSummary()} +} + +// Parse parses a log line and adds it to the summary. +func (p *Parser) Parse(line string) { + // if there was an error do not continue + if p.lerr != nil { + return + } + + // chain the parser's error to the result's + res, err := parse(line) + if p.lines++; err != nil { + p.lerr = fmt.Errorf("line #%d: %s", p.lines, err) + return + } + + p.summary.update(res) +} + +// Summarize summarizes the parsing results. +// Only use it after the parsing is done. +func (p *Parser) Summarize() *Summary { + return p.summary +} + +// Err returns the last error encountered +func (p *Parser) Err() error { + return p.lerr +} diff --git a/28-methods/logparser-testing/report/parser_test.go b/28-methods/logparser-testing/report/parser_test.go new file mode 100644 index 0000000..b0a1dce --- /dev/null +++ b/28-methods/logparser-testing/report/parser_test.go @@ -0,0 +1,55 @@ +package report_test + +import ( + "strings" + "testing" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +func newParser(lines string) *report.Parser { + p := report.New() + p.Parse(lines) + return p +} + +func TestParserLineErrs(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("b.com -1 -1") + + want := "#2" + err := p.Err().Error() + + if !strings.Contains(err, want) { + t.Errorf("want: %q; got: %q", want, err) + } +} + +func TestParserStopsOnErr(t *testing.T) { + p := newParser("a.com 10 20") + p.Parse("b.com -1 -1") + p.Parse("neverparses.com 30 40") + + s := p.Summarize() + if want, got := 10, s.Total().Visits; want != got { + t.Errorf("want: %d; got: %d", want, got) + } +} + +func TestParserIncorrectFields(t *testing.T) { + tests := []struct { + in, name string + }{ + {"a.com", "missing fields"}, + {"a.com -1 2", "incorrect visits"}, + {"a.com 1 -1", "incorrect time spent"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if p := newParser(tt.in); p.Err() == nil { + t.Errorf("in: %q; got: nil err", tt.in) + } + }) + } +} diff --git a/28-methods/logparser-testing/report/result.go b/28-methods/logparser-testing/report/result.go new file mode 100644 index 0000000..95e0022 --- /dev/null +++ b/28-methods/logparser-testing/report/result.go @@ -0,0 +1,60 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "fmt" + "strconv" + "strings" +) + +// always put all the related things together as in here + +// Result stores metrics for a domain +// it uses the value mechanics, +// because it doesn't have to update anything +type Result struct { + Domain string `json:"domain"` + Visits int `json:"visits"` + TimeSpent int `json:"time_spent"` + // add more metrics if needed +} + +// add adds the metrics of another Result to itself and returns a new Result +func (r Result) add(other Result) Result { + return Result{ + Domain: r.Domain, + Visits: r.Visits + other.Visits, + TimeSpent: r.TimeSpent + other.TimeSpent, + } +} + +// parse parses a single log line +func parse(line string) (r Result, err error) { + fields := strings.Fields(line) + if len(fields) != 3 { + return r, fmt.Errorf("missing fields: %v", fields) + } + + f := new(field) + r.Domain = fields[0] + r.Visits = f.atoi("visits", fields[1]) + r.TimeSpent = f.atoi("time spent", fields[2]) + return r, f.err +} + +// field helps for field parsing +type field struct{ err error } + +func (f *field) atoi(name, val string) int { + n, err := strconv.Atoi(val) + if n < 0 || err != nil { + f.err = fmt.Errorf("incorrect %s: %q", name, val) + } + return n +} diff --git a/28-methods/logparser-testing/report/summary.go b/28-methods/logparser-testing/report/summary.go new file mode 100644 index 0000000..f1b88bd --- /dev/null +++ b/28-methods/logparser-testing/report/summary.go @@ -0,0 +1,86 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "sort" +) + +// Summary aggregates the parsing results +type Summary struct { + sum map[string]Result // metrics per domain + domains []string // unique domain names + total Result // total visits for all domains +} + +// newSummary constructs and initializes a new summary +// You can't use its methods without pointer mechanics +func newSummary() *Summary { + return &Summary{sum: make(map[string]Result)} +} + +// Update updates the report for the given parsing result +func (s *Summary) update(r Result) { + domain := r.Domain + if _, ok := s.sum[domain]; !ok { + s.domains = append(s.domains, domain) + } + + // let the result handle the addition + // this allows us to manage the result in once place + // and this way it becomes easily extendable + s.total = s.total.add(r) + s.sum[domain] = r.add(s.sum[domain]) +} + +// Iterator returns `next()` to detect when the iteration ends, +// and a `cur()` to return the current result. +// iterator iterates sorted by domains. +func (s *Summary) Iterator() (next func() bool, cur func() Result) { + sort.Strings(s.domains) + + // remember the last iterated result + var last int + + next = func() bool { + defer func() { last++ }() + return len(s.domains) > last + } + + cur = func() Result { + // returns a copy so the caller cannot change it + name := s.domains[last-1] + return s.sum[name] + } + + return +} + +// Total returns the total metrics +func (s *Summary) Total() Result { + return s.total +} + +// For the interfaces section +// +// MarshalJSON marshals a report to JSON +// Alternative: unexported embedding +// func (s *Summary) MarshalJSON() ([]byte, error) { +// type total struct { +// *Result +// IgnoreDomain *string `json:"domain,omitempty"` +// } + +// return json.Marshal(struct { +// Sum map[string]Result `json:"summary"` +// Domains []string `json:"domains"` +// Total total `json:"total"` +// }{ +// Sum: s.sum, Domains: s.domains, Total: total{Result: &s.total}, +// }) +// } diff --git a/28-methods/logparser-testing/report/summary_test.go b/28-methods/logparser-testing/report/summary_test.go new file mode 100644 index 0000000..2a0fc50 --- /dev/null +++ b/28-methods/logparser-testing/report/summary_test.go @@ -0,0 +1,44 @@ +package report_test + +import ( + "testing" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +func TestSummaryTotal(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("b.com 3 4") + + s := p.Summarize() + + want := report.Result{Domain: "", Visits: 4, TimeSpent: 6} + if got := s.Total(); want != got { + t.Errorf("want: %+v; got: %+v", want, got) + } +} + +func TestSummaryIterator(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("a.com 3 4") + p.Parse("b.com 5 6") + + s := p.Summarize() + next, cur := s.Iterator() + + wants := []report.Result{ + {Domain: "a.com", Visits: 4, TimeSpent: 6}, + {Domain: "b.com", Visits: 5, TimeSpent: 6}, + } + + for _, want := range wants { + t.Run(want.Domain, func(t *testing.T) { + if got := next(); !got { + t.Errorf("next(): want: %t; got: %t", true, got) + } + if got := cur(); want != got { + t.Errorf("cur(): want: %+v; got: %+v", want, got) + } + }) + } +} diff --git a/28-methods/logparser-testing/summarize.go b/28-methods/logparser-testing/summarize.go new file mode 100644 index 0000000..63d34c0 --- /dev/null +++ b/28-methods/logparser-testing/summarize.go @@ -0,0 +1,73 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +// summarize prints the parsing results. +// +// it prints the errors and returns if there are any. +// +// --json flag encodes to json and prints. +func summarize(sum *report.Summary, errors ...error) { + if errs(errors...) { + return + } + + if args := os.Args[1:]; len(args) == 1 && args[0] == "--json" { + encode(sum) + return + } + stdout(sum) +} + +// encodes the summary to json +func encode(sum *report.Summary) { + out, err := json.MarshalIndent(sum, "", "\t") + if err != nil { + panic(err) + } + os.Stdout.Write(out) +} + +// prints the summary to standard out +func stdout(sum *report.Summary) { + const ( + head = "%-30s %10s %20s\n" + val = "%-30s %10d %20d\n" + ) + + fmt.Printf(head, "DOMAIN", "VISITS", "TIME SPENT") + fmt.Println(strings.Repeat("-", 65)) + + for next, cur := sum.Iterator(); next(); { + r := cur() + fmt.Printf(val, r.Domain, r.Visits, r.TimeSpent) + } + + t := sum.Total() + fmt.Printf("\n"+val, "TOTAL", t.Visits, t.TimeSpent) +} + +// this variadic func simplifies the multiple error handling +func errs(errs ...error) (wasErr bool) { + for _, err := range errs { + if err != nil { + fmt.Printf("> Err: %s\n", err) + wasErr = true + } + } + return +}