diff options
Diffstat (limited to 'go/src')
-rw-r--r-- | go/src/regtest.go | 274 | ||||
-rw-r--r-- | go/src/xdelta/rstream.go | 71 | ||||
-rw-r--r-- | go/src/xdelta/run.go | 71 | ||||
-rw-r--r-- | go/src/xdelta/test.go | 164 | ||||
-rw-r--r-- | go/src/xdelta/tgroup.go | 97 |
5 files changed, 677 insertions, 0 deletions
diff --git a/go/src/regtest.go b/go/src/regtest.go new file mode 100644 index 0000000..9d91f69 --- /dev/null +++ b/go/src/regtest.go @@ -0,0 +1,274 @@ +package main + +import ( + "fmt" + "io" + "path" + "os" + "sort" + "time" + + "xdelta" +) + +const ( + xdataset = "/volume/home/jmacd/src/testdata" + xcompare = "/volume/home/jmacd/src/xdelta-devel/xdelta3/build/x86_64-pc-linux-gnu-m64/xoff64/xdelta3" + xdelta3 = "/volume/home/jmacd/src/xdelta-64bithash/xdelta3/build/x86_64-pc-linux-gnu-m64/usize64/xoff64/xdelta3" + seed = 1422253499919909358 +) + +type Config struct { + srcbuf_size int64 + window_size int64 + blocksize int +} + +func NewC() Config { + // TODO make these (and above) flags + return Config{1<<26, 1<<22, 1<<16} +} + +func (c Config) smokeTest(t *xdelta.TestGroup, p xdelta.Program) { + target := "Hello world!" + source := "Hello world, nice to meet you!" + + enc, err := t.Exec("encode", p, true, []string{"-e"}) + if err != nil { + t.Panic(err) + } + dec, err := t.Exec("decode", p, true, []string{"-d"}) + if err != nil { + t.Panic(err) + } + + encodeout := t.Drain(enc.Stdout, "encode.stdout") + decodeout := t.Drain(dec.Stdout, "decode.stdout") + + t.Empty(enc.Stderr, "encode") + t.Empty(dec.Stderr, "decode") + + t.TestWrite("encode.stdin", enc.Stdin, []byte(target)) + t.TestWrite("encode.srcin", enc.Srcin, []byte(source)) + + t.TestWrite("decode.stdin", dec.Stdin, <-encodeout) + t.TestWrite("decode.srcin", dec.Srcin, []byte(source)) + + if do := string(<-decodeout); do != target { + t.Panic(fmt.Errorf("It's not working! %s\n!=\n%s\n", do, target)) + } + t.Wait(enc, dec) +} + +type PairTest struct { + // Input + Config + program xdelta.Program + source, target string + + // Output + TestOutput +} + +type TestOutput struct { + encoded int64 + encDuration time.Duration + decDuration time.Duration + encSysDuration time.Duration + decSysDuration time.Duration +} + +func (to *TestOutput) Add(a TestOutput) { + to.encoded += a.encoded + to.encDuration += a.encDuration + to.decDuration += a.decDuration + to.encSysDuration += a.encSysDuration + to.decSysDuration += a.decSysDuration +} + +func (to *TestOutput) String() string { + return fmt.Sprintf("SIZE: %v\tT: %v\tTSYS: %v\tDT: %v\tDTSYS: %v", + to.encoded, to.encDuration, to.encSysDuration, to.decDuration, to.encSysDuration) +} + +// P is the test program, Q is the reference version. +func (cfg Config) datasetTest(t *xdelta.TestGroup, p, q xdelta.Program) { + dir, err := os.Open(xdataset) + if err != nil { + t.Panic(err) + } + dents, err := dir.Readdir(-1) + if err != nil { + t.Panic(err) + } + paths := make([]string, len(dents)) + var total int64 + for i, d := range dents { + if !d.Mode().IsRegular() { + continue + } + paths[i] = fmt.Sprint(xdataset, "/", d.Name()) + total += d.Size() + } + meansize := total / int64(len(dents)) + largest := uint(20) + for ; largest <= 31 && 1<<largest < meansize; largest++ {} + + sort.Strings(paths) + + testSum := map[uint]*TestOutput{} + compSum := map[uint]*TestOutput{} + + for _, in1 := range paths { + for _, in2 := range paths { + if in1 == in2 { continue } + + // 1/4, 1/2, and 1 of the power-of-2 rounded-up mean size + for b := largest - 2; b <= largest; b++ { + if _, has := testSum[b]; !has { + testSum[b] = &TestOutput{} + compSum[b] = &TestOutput{} + } + c1 := cfg + c1.srcbuf_size = 1<<b + ptest := &PairTest{c1, p, in1, in2, TestOutput{-1, 0, 0, 0, 0}} + ptest.datasetPairTest(t, 1<<b); + qtest := &PairTest{c1, q, in1, in2, TestOutput{-1, 0, 0, 0, 0}} + qtest.datasetPairTest(t, 1<<b) + + testSum[b].Add(ptest.TestOutput) + compSum[b].Add(qtest.TestOutput) + + fmt.Printf("%s, %s: %.2f%% %+d/%d\n\tE:%.2f%%/%s(%.2f%%/%s) D:%.2f%%/%s(%.2f%%/%s) [B=%d]\n", + path.Base(in1), path.Base(in2), + float64(ptest.encoded - qtest.encoded) * 100.0 / float64(qtest.encoded), + ptest.encoded - qtest.encoded, + qtest.encoded, + (ptest.encDuration - qtest.encDuration).Seconds() * 100.0 / qtest.encDuration.Seconds(), + qtest.encDuration, + (ptest.decDuration - qtest.decDuration).Seconds() * 100.0 / qtest.decDuration.Seconds(), + qtest.encDuration, + (ptest.encSysDuration - qtest.encSysDuration).Seconds() * 100.0 / qtest.encSysDuration.Seconds(), + qtest.encSysDuration, + (ptest.decSysDuration - qtest.decSysDuration).Seconds() * 100.0 / qtest.decSysDuration.Seconds(), + qtest.decSysDuration, + 1<<b) + } + } + } + var keys []uint + for k, _ := range testSum { + keys = append(keys, k) + } + for _, k := range keys { + fmt.Printf("B=%v\nTEST: %v\nCOMP: %v\n", 1<<k, testSum[k], compSum[k]) + } +} + +func (pt *PairTest) datasetPairTest(t *xdelta.TestGroup, meanSize int64) { + cfg := pt.Config + eargs := []string{"-e", fmt.Sprint("-B", cfg.srcbuf_size), // "-q", + fmt.Sprint("-W", cfg.window_size), "-s", pt.source, + "-I0", "-S", "none", pt.target} + enc, err := t.Exec("encode", pt.program, false, eargs) + if err != nil { + t.Panic(err) + } + + dargs := []string{"-dc", fmt.Sprint("-B", cfg.srcbuf_size), //"-q", + fmt.Sprint("-W", cfg.window_size), "-s", pt.source, + "-S", "none"} + + dec, err := t.Exec("decode", pt.program, false, dargs) + if err != nil { + t.Panic(err) + } + tgt_check, err := os.Open(pt.target) + if err != nil { + t.Panic(err) + } + tgt_info, err := tgt_check.Stat() + if err != nil { + t.Panic(err) + } + t.Empty(enc.Stderr, "encode") + t.Empty(dec.Stderr, "decode") + t.CopyStreams(enc.Stdout, dec.Stdin, &pt.encoded) + t.CompareStreams(dec.Stdout, tgt_check, tgt_info.Size()) + + t.Wait(enc, dec) + + pt.decDuration = dec.Cmd.ProcessState.UserTime() + pt.encDuration = enc.Cmd.ProcessState.UserTime() + pt.decSysDuration = dec.Cmd.ProcessState.SystemTime() + pt.encSysDuration = enc.Cmd.ProcessState.SystemTime() +} + +func (cfg Config) offsetTest(t *xdelta.TestGroup, p xdelta.Program, offset, length int64) { + eargs := []string{"-e", "-0", fmt.Sprint("-B", cfg.srcbuf_size), "-q", + fmt.Sprint("-W", cfg.window_size)} + enc, err := t.Exec("encode", p, true, eargs) + if err != nil { + t.Panic(err) + } + + dargs := []string{"-d", fmt.Sprint("-B", cfg.srcbuf_size), "-q", + fmt.Sprint("-W", cfg.window_size)} + dec, err := t.Exec("decode", p, true, dargs) + if err != nil { + t.Panic(err) + } + + // The pipe used to read the decoder output and compare + // against the target. + read, write := io.Pipe() + + t.Empty(enc.Stderr, "encode") + t.Empty(dec.Stderr, "decode") + + var encoded_size int64 + t.CopyStreams(enc.Stdout, dec.Stdin, &encoded_size) + t.CompareStreams(dec.Stdout, read, length) + + // The decoder output ("read", above) is compared with the + // test-provided output ("write", below). The following + // generates two identical inputs. + t.WriteRstreams("encode", seed, offset, length, enc.Srcin, enc.Stdin) + t.WriteRstreams("decode", seed, offset, length, dec.Srcin, write) + t.Wait(enc, dec) + + expect := cfg.srcbuf_size - offset + if float64(encoded_size) < (0.95 * float64(expect)) || + float64(encoded_size) > (1.05 * float64(expect)) { + t.Fail("encoded size should be ~=", expect, ", actual ", encoded_size) + } +} + +func main() { + r, err := xdelta.NewRunner() + if err != nil { + panic(err) + } + defer r.Cleanup() + + cfg := NewC() + + prog := xdelta.Program{xdelta3} + + r.RunTest("smoketest", func(t *xdelta.TestGroup) { cfg.smokeTest(t, prog) }) + + for i := uint(29); i <= 33; i += 1 { + // The arguments to offsetTest are offset, source + // window size, and file size. The source window size + // is (2 << i) and (in the 3.0x release branch) is + // limited to 2^31, so the the greatest value of i is + // 30. + cfg.srcbuf_size = 2 << i + r.RunTest(fmt.Sprint("offset", i), func(t *xdelta.TestGroup) { + cfg.offsetTest(t, prog, 1 << i, 3 << i) }) + } + + comp := xdelta.Program{xcompare} + + r.RunTest("dataset", func(t *xdelta.TestGroup) { cfg.datasetTest(t, prog, comp) }) +} diff --git a/go/src/xdelta/rstream.go b/go/src/xdelta/rstream.go new file mode 100644 index 0000000..99c3d17 --- /dev/null +++ b/go/src/xdelta/rstream.go @@ -0,0 +1,71 @@ +package xdelta + + +import ( + "io" + "math/rand" +) + +const ( + blocksize = 1<<17 +) + +func (t *TestGroup) WriteRstreams(desc string, seed, offset, len int64, + src, tgt io.WriteCloser) { + t.Go("src-write:"+desc, func (g *Goroutine) { + writeOne(g, seed, 0, len, tgt, false) + }) + t.Go("tgt-write:"+desc, func (g *Goroutine) { + writeOne(g, seed, offset, len, src, true) + }) +} + +func writeOne(g *Goroutine, seed, offset, len int64, stream io.WriteCloser, readall bool) { + if !readall { + // Allow the source-read to fail or block until the process terminates. + // This behavior is reserved for the decoder, which is not required to + // read the entire source. + g.OK() + } + if offset != 0 { + // Fill with other random data until the offset + if err := writeRand(g, rand.New(rand.NewSource(^seed)), offset, stream); err != nil { + g.Panic(err) + } + } + if err := writeRand(g, rand.New(rand.NewSource(seed)), + len - offset, stream); err != nil { + g.Panic(err) + } + if err := stream.Close(); err != nil { + g.Panic(err) + } + g.OK() +} + +func writeRand(g *Goroutine, r *rand.Rand, len int64, s io.Writer) error { + blk := make([]byte, blocksize) + for len > 0 { + fillRand(r, blk) + c := blocksize + if len < blocksize { + c = int(len) + } + if _, err := s.Write(blk[0:c]); err != nil { + return err + } + len -= int64(c) + } + return nil +} + +func fillRand(r *rand.Rand, blk []byte) { + for p := 0; p < len(blk); { + v := r.Int63() + for i := 7; i != 0 && p < len(blk); i-- { + blk[p] = byte(v) + p++ + v >>= 8 + } + } +} diff --git a/go/src/xdelta/run.go b/go/src/xdelta/run.go new file mode 100644 index 0000000..448fabe --- /dev/null +++ b/go/src/xdelta/run.go @@ -0,0 +1,71 @@ +package xdelta + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" +) + +type Program struct { + Path string +} + +type Run struct { + Cmd exec.Cmd + Srcfile string + Stdin io.WriteCloser + Srcin io.WriteCloser + Stdout io.ReadCloser + Stderr io.ReadCloser +} + +type Runner struct { + Testdir string +} + +func (r *Run) Wait() error { + return r.Cmd.Wait() +} + +func NewRunner() (*Runner, error) { + if dir, err := ioutil.TempDir(tmpDir, "xrt"); err != nil { + return nil, err + } else { + return &Runner{dir}, nil + } +} + +func (r *Runner) newTestGroup(name string) (*TestGroup) { + tg := &TestGroup{Runner: r} + tg.WaitGroup.Add(1) + g0 := &Goroutine{tg, name, false} + tg.running = append(tg.running, g0) + tg.main = g0 + return tg +} + +func (r *Runner) Cleanup() { + os.RemoveAll(r.Testdir) +} + +func (r *Runner) RunTest(name string, f func (t *TestGroup)) { + t := r.newTestGroup(name) + c := make(chan interface{}) + go func() { + defer func() { + rec := recover() + c <- rec + }() + fmt.Println("Testing", name, "...") + f(t) + c <- nil + }() + rec := <- c + if t.errors == nil && rec == nil { + fmt.Println("Success:", name) + } else { + fmt.Println("FAILED:", name, t.errors, rec) + } +} diff --git a/go/src/xdelta/test.go b/go/src/xdelta/test.go new file mode 100644 index 0000000..7210698 --- /dev/null +++ b/go/src/xdelta/test.go @@ -0,0 +1,164 @@ +package xdelta + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "sync/atomic" + + "golang.org/x/sys/unix" +) + +var ( + tmpDir = "/tmp" + srcSeq int64 +) + +func (t *TestGroup) Drain(f io.ReadCloser, desc string) <-chan []byte { + c := make(chan []byte) + t.Go(desc, func(g *Goroutine) { + if b, err := ioutil.ReadAll(f); err != nil { + g.Panic(err) + } else { + c <- b + } + g.OK() + }) + return c +} + +func (t *TestGroup) Empty(f io.ReadCloser, desc string) *Goroutine { + return t.Go("empty:"+desc, func (g *Goroutine) { + s := bufio.NewScanner(f) + for s.Scan() { + os.Stderr.Write([]byte(fmt.Sprint(desc, ": ", s.Text(), "\n"))) + } + err := s.Err() + f.Close() + if err != nil { + g.Panic(err) + } + g.OK() + }) +} + +func (t *TestGroup) TestWrite(what string, f io.WriteCloser, b []byte) *Goroutine { + return t.Go("write", func(g *Goroutine) { + if _, err := f.Write(b); err != nil { + g.Panic(err) + } + if err := f.Close(); err != nil { + g.Panic(err) + } + g.OK() + }) +} + +func (t *TestGroup) CopyStreams(r io.ReadCloser, w io.WriteCloser, written *int64) *Goroutine { + return t.Go("copy", func(g *Goroutine) { + nwrite, err := io.Copy(w, r) + if err != nil { + g.Panic(err) + } + err = r.Close() + if err != nil { + g.Panic(err) + } + err = w.Close() + if err != nil { + g.Panic(err) + } + g.OK() + *written = nwrite + }) +} + +func (t *TestGroup) CompareStreams(r1 io.ReadCloser, r2 io.ReadCloser, length int64) *Goroutine { + return t.Go("compare", func(g *Goroutine) { + b1 := make([]byte, blocksize) + b2 := make([]byte, blocksize) + var idx int64 + for length > 0 { + c := blocksize + if length < blocksize { + c = int(length) + } + if _, err := io.ReadFull(r1, b1[0:c]); err != nil { + g.Panic(err) + } + if _, err := io.ReadFull(r2, b2[0:c]); err != nil { + g.Panic(err) + } + if bytes.Compare(b1[0:c], b2[0:c]) != 0 { + fmt.Println("B1 is", string(b1[0:c])) + fmt.Println("B2 is", string(b2[0:c])) + g.Panic(errors.New(fmt.Sprint("Bytes do not compare at ", idx))) + } + length -= int64(c) + idx += int64(c) + } + g.OK() + }) +} + +func (t *TestGroup) Exec(desc string, p Program, srcfifo bool, flags []string) (*Run, error) { + var err error + run := &Run{} + args := []string{p.Path} + if srcfifo { + num := atomic.AddInt64(&srcSeq, 1) + run.Srcfile = path.Join(t.Runner.Testdir, fmt.Sprint("source", num)) + if err = unix.Mkfifo(run.Srcfile, 0600); err != nil { + return nil, err + } + read, write := io.Pipe() + t.writeFifo(run.Srcfile, read) + run.Srcin = write + args = append(args, "-s") + args = append(args, run.Srcfile) + } + if run.Stdin, err = run.Cmd.StdinPipe(); err != nil { + return nil, err + } + if run.Stdout, err = run.Cmd.StdoutPipe(); err != nil { + return nil, err + } + if run.Stderr, err = run.Cmd.StderrPipe(); err != nil { + return nil, err + } + + run.Cmd.Path = p.Path + run.Cmd.Args = append(args, flags...) + run.Cmd.Dir = t.Runner.Testdir + if serr := run.Cmd.Start(); serr != nil { + return nil, serr + } + return run, nil +} + +func (t *TestGroup) Fail(v ...interface{}) { + panic(fmt.Sprintln(v...)) +} + +func (t *TestGroup) writeFifo(srcfile string, read io.Reader) *Goroutine { + return t.Go("compare", func(g *Goroutine) { + fifo, err := os.OpenFile(srcfile, os.O_WRONLY, 0600) + if err != nil { + fifo.Close() + g.Panic(err) + } + if _, err := io.Copy(fifo, read); err != nil { + fifo.Close() + g.Panic(err) + } + if err := fifo.Close(); err != nil { + g.Panic(err) + } + g.OK() + }) +} diff --git a/go/src/xdelta/tgroup.go b/go/src/xdelta/tgroup.go new file mode 100644 index 0000000..602b1e1 --- /dev/null +++ b/go/src/xdelta/tgroup.go @@ -0,0 +1,97 @@ +package xdelta + +import ( + "fmt" + "runtime" + "sync" +) + +type TestGroup struct { + *Runner + main *Goroutine + sync.Mutex + sync.WaitGroup + running []*Goroutine + errors []error + nonerrors []error // For tolerated / expected conditions +} + +type Goroutine struct { + *TestGroup + name string + done bool +} + +func (g *Goroutine) String() string { + return fmt.Sprint("[", g.name, "]") +} + +func (g *Goroutine) finish(err error) { + wait := false + tg := g.TestGroup + sbuf := make([]byte, 4096) + sbuf = sbuf[0:runtime.Stack(sbuf, false)] + if err != nil { + err = fmt.Errorf("%v:%v:%v", g.name, err, string(sbuf)) + } + tg.Lock() + if g.done { + if err != nil { + tg.nonerrors = append(tg.nonerrors, err) + } + } else { + wait = true + g.done = true + if err != nil { + tg.errors = append(tg.errors, err) + } + } + tg.Unlock() + if wait { + tg.WaitGroup.Done() + } +} + +func (g *Goroutine) OK() { + g.finish(nil) +} + +func (g *Goroutine) Panic(err error) { + g.finish(err) + if g != g.TestGroup.main { + runtime.Goexit() + } +} + +func (t *TestGroup) Main() *Goroutine { return t.main } + +func (t *TestGroup) Panic(err error) { t.Main().Panic(err) } + +func (t *TestGroup) Go(name string, f func(*Goroutine)) *Goroutine { + g := &Goroutine{t, name, false} + t.Lock() + t.WaitGroup.Add(1) + t.running = append(t.running, g) + t.Unlock() + go f(g) + return g +} + +func (t *TestGroup) Wait(procs... *Run) { + t.Main().OK() + t.WaitGroup.Wait() + for _, p := range procs { + if err := p.Wait(); err != nil { + t.errors = append(t.errors, err) + } + } + for _, err := range t.errors { + fmt.Println(":ERROR:", err) + } + for _, err := range t.nonerrors { + fmt.Println("(ERROR)", err) + } + if len(t.errors) != 0 { + t.Fail("Test failed with", len(t.errors), "errors") + } +} |