// +build fuzzy package xlsx import ( "archive/zip" "bytes" "encoding/xml" "flag" "fmt" "io" "log" "math/rand" "path/filepath" "reflect" "strconv" "testing" "time" . "gopkg.in/check.v1" ) type Fuzzy struct{} var _ = Suite(&Fuzzy{}) var randseed *int64 = flag.Int64("test.seed", time.Now().Unix(), "Set the random seed of the test for repeatable results") type tokenchange struct { file bytes.Buffer old xml.Token new xml.Token } type filechange struct { File *zip.Reader Name string Old xml.Token New xml.Token } var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") var numbers = []rune("0123456789") func randString(n int) []byte { b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return []byte(string(b)) } func randInt(n int) []byte { b := make([]rune, n) for i := range b { b[i] = numbers[rand.Intn(len(numbers))] } return []byte(string(b)) } //This function creates variations on tokens without regards as to positions in the file. func getTokenVariations(t xml.Token) []xml.Token { var result []xml.Token = make([]xml.Token, 0) switch t := t.(type) { case xml.CharData: { //If the token is a number try some random number if _, err := strconv.Atoi(string(t)); err == nil { result = append(result, xml.CharData(randInt(rand.Intn(15)))) } result = append(result, xml.CharData(randString(rand.Intn(100)))) return result } case xml.StartElement: { for k := range t.Attr { if _, err := strconv.Atoi(string(t.Attr[k].Value)); err == nil { start := xml.CopyToken(t).(xml.StartElement) start.Attr[k].Value = string(randInt(rand.Intn(15))) result = append(result, start) } start := xml.CopyToken(t).(xml.StartElement) start.Attr[k].Value = string(randString(rand.Intn(100))) result = append(result, start) } return result } default: { return make([]xml.Token, 0) // No variations on non char tokens yet } } } func variationsXML(f *zip.File) chan tokenchange { result := make(chan tokenchange) r, _ := f.Open() xmlReader := xml.NewDecoder(r) var tokenList []xml.Token for { if t, err := xmlReader.Token(); err == nil { tokenList = append(tokenList, xml.CopyToken(t)) } else { break } } go func() { //Over every token we want to break for TokenToBreak := range tokenList { //Get the ways we can break that token for _, brokenToken := range getTokenVariations(tokenList[TokenToBreak]) { var buf bytes.Buffer xmlWriter := xml.NewEncoder(&buf) //Now create an xml file where one token is broken for currentToken, t := range tokenList { if currentToken == TokenToBreak { xmlWriter.EncodeToken(brokenToken) } else { xmlWriter.EncodeToken(t) } } xmlWriter.Flush() result <- tokenchange{buf, tokenList[TokenToBreak], brokenToken} } } close(result) }() return result } func generateBrokenFiles(r *zip.Reader) chan filechange { result := make(chan filechange) go func() { count := 0 //For every file in the zip we want variation on for breakIndex, fileToBreak := range r.File { if filepath.Ext(fileToBreak.Name) != ".xml" { continue //We cannot create variations on non-xml files } variationCount := 0 //For every broken version of that file for changedFile := range variationsXML(fileToBreak) { variationCount++ var buffer bytes.Buffer //Create a new xlsx file in memory outZip := zip.NewWriter(&buffer) w, err := outZip.Create(fileToBreak.Name) if err != nil { log.Fatal(err) } //Add modified file to xlsx _, err = changedFile.file.WriteTo(w) if err != nil { log.Fatal("changedFile.file.WriteTo", err) } //Add other, unchanged, files. for otherIndex, otherFile := range r.File { if breakIndex == otherIndex { continue } to, err := outZip.Create(otherFile.Name) if err != nil { log.Fatal("Could not add new file to xlsx due to", err) } from, err := otherFile.Open() if err != nil { log.Fatal("Could not open original file from template xlsx due to", err) } io.Copy(to, from) from.Close() } outZip.Close() //Return this combination of broken files b := buffer.Bytes() var res filechange res.File, _ = zip.NewReader(bytes.NewReader(b), int64(len(b))) res.Name = fileToBreak.Name res.Old = changedFile.old res.New = changedFile.new result <- res count++ } } close(result) }() return result } func Raises(f func()) (err interface{}) { defer func() { err = recover() }() err = nil f() return } func tokenToString(t xml.Token) string { switch t := t.(type) { case xml.CharData: { return string(t) } default: { return fmt.Sprint(t) } } } func (f *Fuzzy) TestRandomBrokenParts(c *C) { if testing.Short() { c.Log("This test, tests many versions of an xlsx file and might take a while, it is being skipped") c.SucceedNow() } log.Println("Fuzzy test is using this -test.seed=" + strconv.FormatInt(*randseed, 10)) rand.Seed(*randseed) template, err := zip.OpenReader("./testdocs/testfile.xlsx") c.Assert(err, IsNil) defer template.Close() count := 0 for brokenFile := range generateBrokenFiles(&template.Reader) { count++ if testing.Verbose() { //If the library panics fatally it would be nice to know why log.Println("Testing change to ", brokenFile.Name, " on token ", tokenToString(brokenFile.Old), " of type ", reflect.TypeOf(brokenFile.Old), " to ", tokenToString(brokenFile.New)) } if e := Raises(func() { ReadZipReader(brokenFile.File) }); e != nil { c.Log("Some file with random changes did raise an exception instead of returning an error", e) c.Log("Testing change to ", brokenFile.Name, " on token ", tokenToString(brokenFile.Old), " of type ", reflect.TypeOf(brokenFile.Old), " to ", tokenToString(brokenFile.New)) c.FailNow() } } c.Succeed() }