move: log parsers
This commit is contained in:
6
logparser/testing/log.txt
Normal file
6
logparser/testing/log.txt
Normal file
@ -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
|
6
logparser/testing/log_err_missing.txt
Normal file
6
logparser/testing/log_err_missing.txt
Normal file
@ -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
|
6
logparser/testing/log_err_negative.txt
Normal file
6
logparser/testing/log_err_negative.txt
Normal file
@ -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
|
6
logparser/testing/log_err_str.txt
Normal file
6
logparser/testing/log_err_str.txt
Normal file
@ -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
|
26
logparser/testing/main.go
Normal file
26
logparser/testing/main.go
Normal file
@ -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/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())
|
||||
}
|
59
logparser/testing/main_test.go
Normal file
59
logparser/testing/main_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
// +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", ".")
|
||||
cmd.Stdin = strings.NewReader(in)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
52
logparser/testing/report/parser.go
Normal file
52
logparser/testing/report/parser.go
Normal file
@ -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
|
||||
}
|
55
logparser/testing/report/parser_test.go
Normal file
55
logparser/testing/report/parser_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package report_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/inancgumus/learngo/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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
logparser/testing/report/result.go
Normal file
60
logparser/testing/report/result.go
Normal file
@ -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
|
||||
}
|
86
logparser/testing/report/summary.go
Normal file
86
logparser/testing/report/summary.go
Normal file
@ -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},
|
||||
// })
|
||||
// }
|
44
logparser/testing/report/summary_test.go
Normal file
44
logparser/testing/report/summary_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package report_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inancgumus/learngo/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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
73
logparser/testing/summarize.go
Normal file
73
logparser/testing/summarize.go
Normal file
@ -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/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
|
||||
}
|
Reference in New Issue
Block a user