main.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // Command xpjdiff compares two MPC Sample projects: the decompressed .xpj
  2. // files and the sample files in their data folders. It prints the paths of
  3. // the values that differ, or "identical". Works the same in cmd,
  4. // PowerShell and sh:
  5. //
  6. // go run ./tools/xpjdiff A.xpj B.xpj
  7. //
  8. // The project name is not part of the comparison (it lives only in the file
  9. // and folder names), so A and B may be named differently.
  10. package main
  11. import (
  12. "bytes"
  13. "fmt"
  14. "os"
  15. "path/filepath"
  16. "sort"
  17. "strings"
  18. "git.monoexp.com/bajdev/mpckit/internal/project"
  19. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  20. )
  21. const maxShown = 40
  22. func main() {
  23. if len(os.Args) != 3 {
  24. fmt.Fprintln(os.Stderr, "usage: xpjdiff A.xpj B.xpj")
  25. os.Exit(2)
  26. }
  27. n, err := compare(os.Args[1], os.Args[2])
  28. if err != nil {
  29. fmt.Fprintln(os.Stderr, "xpjdiff:", err)
  30. os.Exit(2)
  31. }
  32. if n > 0 {
  33. os.Exit(1)
  34. }
  35. fmt.Println("identical")
  36. }
  37. // compare prints the differences and returns how many there are.
  38. func compare(a, b string) (int, error) {
  39. fa, err := xpj.ReadFile(a)
  40. if err != nil {
  41. return 0, fmt.Errorf("%s: %w", a, err)
  42. }
  43. fb, err := xpj.ReadFile(b)
  44. if err != nil {
  45. return 0, fmt.Errorf("%s: %w", b, err)
  46. }
  47. ea, err := fa.Encode()
  48. if err != nil {
  49. return 0, err
  50. }
  51. eb, err := fb.Encode()
  52. if err != nil {
  53. return 0, err
  54. }
  55. var diffs []string
  56. if !bytes.Equal(ea, eb) {
  57. if strings.Join(fa.Header, "\n") != strings.Join(fb.Header, "\n") {
  58. diffs = append(diffs, fmt.Sprintf("header: %q vs %q", fa.Header, fb.Header))
  59. }
  60. la, lb := map[string]string{}, map[string]string{}
  61. leaves(fa.Root, "", la)
  62. leaves(fb.Root, "", lb)
  63. for k, v := range la {
  64. if w, ok := lb[k]; !ok {
  65. diffs = append(diffs, fmt.Sprintf("%s: only in A (%s)", k, v))
  66. } else if v != w {
  67. diffs = append(diffs, fmt.Sprintf("%s: %s vs %s", k, v, w))
  68. }
  69. }
  70. for k, v := range lb {
  71. if _, ok := la[k]; !ok {
  72. diffs = append(diffs, fmt.Sprintf("%s: only in B (%s)", k, v))
  73. }
  74. }
  75. if len(diffs) == 0 {
  76. diffs = append(diffs, "same values, different key order or layout")
  77. }
  78. }
  79. diffs = append(diffs, compareData(a, b)...)
  80. sort.Strings(diffs)
  81. for i, d := range diffs {
  82. if i == maxShown {
  83. fmt.Printf("... and %d more\n", len(diffs)-maxShown)
  84. break
  85. }
  86. fmt.Println(d)
  87. }
  88. return len(diffs), nil
  89. }
  90. // leaves flattens a tree into path -> literal value.
  91. func leaves(n *xpj.Node, path string, out map[string]string) {
  92. switch n.Kind {
  93. case xpj.Object:
  94. for _, m := range n.Members {
  95. leaves(m.Value, path+"."+m.Key, out)
  96. }
  97. if len(n.Members) == 0 {
  98. out[path] = "{}"
  99. }
  100. case xpj.Array:
  101. for i, it := range n.Items {
  102. leaves(it, fmt.Sprintf("%s[%d]", path, i), out)
  103. }
  104. if len(n.Items) == 0 {
  105. out[path] = "[]"
  106. }
  107. default:
  108. b, _ := n.MarshalJSON()
  109. out[path] = string(b)
  110. }
  111. }
  112. func dataDir(xpjPath string) string {
  113. return strings.TrimSuffix(xpjPath, filepath.Ext(xpjPath)) + project.DataSuffix
  114. }
  115. // compareData compares the files in the two data folders.
  116. func compareData(a, b string) []string {
  117. da, db := dataDir(a), dataDir(b)
  118. ra, errA := os.ReadDir(da)
  119. rb, errB := os.ReadDir(db)
  120. if errA != nil || errB != nil {
  121. var out []string
  122. if errA != nil {
  123. out = append(out, "data folder A: "+errA.Error())
  124. }
  125. if errB != nil {
  126. out = append(out, "data folder B: "+errB.Error())
  127. }
  128. return out
  129. }
  130. names := map[string]int{}
  131. for _, e := range ra {
  132. names[e.Name()] |= 1
  133. }
  134. for _, e := range rb {
  135. names[e.Name()] |= 2
  136. }
  137. var out []string
  138. for name, in := range names {
  139. switch in {
  140. case 1:
  141. out = append(out, "data file only in A: "+name)
  142. case 2:
  143. out = append(out, "data file only in B: "+name)
  144. default:
  145. x, err1 := os.ReadFile(filepath.Join(da, name))
  146. y, err2 := os.ReadFile(filepath.Join(db, name))
  147. if err1 != nil || err2 != nil || !bytes.Equal(x, y) {
  148. out = append(out, "data file differs: "+name)
  149. }
  150. }
  151. }
  152. return out
  153. }