Add noms merge (#2768)

Add optional merging functionality to noms commit.
noms merge <database> <left-dataset-name> <right-dataset-name> <output-dataset-name>

The command above will look in the given Database for the two named
Datasets and, if possible, merge their HeadValue()s and commit the
result back to <output-dataset-name>.

Fixes #2535
This commit is contained in:
cmasone-attic
2016-10-27 15:27:36 -07:00
committed by GitHub
parent 8bd980553b
commit f2ca3d6e8e
7 changed files with 408 additions and 312 deletions

View File

@@ -21,6 +21,7 @@ var commands = []*util.Command{
nomsDiff,
nomsDs,
nomsLog,
nomsMerge,
nomsMigrate,
nomsServe,
nomsShow,

179
cmd/noms/noms_merge.go Normal file
View File

@@ -0,0 +1,179 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"fmt"
"io"
"os"
"regexp"
"github.com/attic-labs/noms/cmd/util"
"github.com/attic-labs/noms/go/config"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/merge"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/status"
"github.com/attic-labs/noms/go/util/verbose"
flag "github.com/juju/gnuflag"
)
var (
resolver string
nomsMerge = &util.Command{
Run: runMerge,
UsageLine: "merge [options] <database> <left-dataset-name> <right-dataset-name> <output-dataset-name>",
Short: "Merges and commits the head values of two named datasets",
Long: "See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the database argument.\nYu must provide a working database and the names of two Datasets you want to merge. The values at the heads of these Datasets will be merged, put into a new Commit object, and set as the Head of the third provided Dataset name.",
Flags: setupMergeFlags,
Nargs: 1, // if absolute-path not present we read it from stdin
}
datasetRe = regexp.MustCompile("^" + datas.DatasetRe.String() + "$")
)
func setupMergeFlags() *flag.FlagSet {
commitFlagSet := flag.NewFlagSet("merge", flag.ExitOnError)
commitFlagSet.StringVar(&resolver, "policy", "n", "conflict resolution policy for merging. Defaults to 'n', which means no resolution strategy will be applied. Supported values are 'l' (left), 'r' (right) and 'p' (prompt). 'prompt' will bring up a simple command-line prompt allowing you to resolve conflicts by choosing between 'l' or 'r' on a case-by-case basis.")
verbose.RegisterVerboseFlags(commitFlagSet)
return commitFlagSet
}
func checkIfTrue(b bool, format string, args ...interface{}) {
if b {
d.CheckErrorNoUsage(fmt.Errorf(format, args...))
}
}
func runMerge(args []string) int {
cfg := config.NewResolver()
if len(args) != 4 {
d.CheckErrorNoUsage(fmt.Errorf("Incorrect number of arguments"))
}
db, err := cfg.GetDatabase(args[0])
d.CheckError(err)
defer db.Close()
leftDS, rightDS, outDS := resolveDatasets(db, args[1], args[2], args[3])
left, right, ancestor := getMergeCandidates(db, leftDS, rightDS)
policy := decidePolicy(resolver)
pc := newMergeProgressChan()
merged, err := policy(left, right, ancestor, db, pc)
d.CheckErrorNoUsage(err)
close(pc)
_, err = db.SetHead(outDS, db.WriteValue(datas.NewCommit(merged, types.NewSet(leftDS.HeadRef(), rightDS.HeadRef()), types.EmptyStruct)))
d.PanicIfError(err)
if !verbose.Quiet() {
status.Printf("Done")
status.Done()
}
return 0
}
func resolveDatasets(db datas.Database, leftName, rightName, outName string) (leftDS, rightDS, outDS datas.Dataset) {
makeDS := func(dsName string) datas.Dataset {
if !datasetRe.MatchString(dsName) {
d.CheckErrorNoUsage(fmt.Errorf("Invalid dataset %s, must match %s", dsName, datas.DatasetRe.String()))
}
return db.GetDataset(dsName)
}
leftDS = makeDS(leftName)
rightDS = makeDS(rightName)
outDS = makeDS(outName)
return
}
func getMergeCandidates(db datas.Database, leftDS, rightDS datas.Dataset) (left, right, ancestor types.Value) {
leftRef, ok := leftDS.MaybeHeadRef()
checkIfTrue(!ok, "Dataset %s has no data", leftDS.ID())
rightRef, ok := rightDS.MaybeHeadRef()
checkIfTrue(!ok, "Dataset %s has no data", rightDS.ID())
ancestorCommit, ok := getCommonAncestor(leftRef, rightRef, db)
checkIfTrue(!ok, "Datasets %s and %s have no common ancestor", leftDS.ID(), rightDS.ID())
return leftDS.HeadValue(), rightDS.HeadValue(), ancestorCommit.Get(datas.ValueField)
}
func getCommonAncestor(r1, r2 types.Ref, vr types.ValueReader) (a types.Struct, found bool) {
aRef, found := datas.FindCommonAncestor(r1, r2, vr)
if !found {
return
}
v := vr.ReadValue(aRef.TargetHash())
if v == nil {
panic(aRef.TargetHash().String() + " not found")
}
if !datas.IsCommitType(v.Type()) {
panic("Not a commit: " + types.EncodedValueMaxLines(v, 10) + " ...")
}
return v.(types.Struct), true
}
func newMergeProgressChan() chan struct{} {
pc := make(chan struct{}, 128)
go func() {
count := 0
for range pc {
if !verbose.Quiet() {
count++
status.Printf("Applied %d changes...", count)
}
}
}()
return pc
}
func decidePolicy(policy string) merge.Policy {
var resolve merge.ResolveFunc
switch policy {
case "n", "N":
resolve = merge.None
case "l", "L":
resolve = merge.Ours
case "r", "R":
resolve = merge.Theirs
case "p", "P":
resolve = func(aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
return cliResolve(os.Stdin, os.Stdout, aType, bType, a, b, path)
}
default:
d.CheckErrorNoUsage(fmt.Errorf("Unsupported merge policy: %s. Choices are n, l, r and a.", policy))
}
return merge.NewThreeWay(resolve)
}
func cliResolve(in io.Reader, out io.Writer, aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
stringer := func(v types.Value) (s string, success bool) {
switch v := v.(type) {
case types.Bool, types.Number, types.String:
return fmt.Sprintf("%v", v), true
}
return "", false
}
left, lOk := stringer(a)
right, rOk := stringer(b)
if !lOk || !rOk {
return change, merged, false
}
// TODO: Handle removes as well.
fmt.Fprintf(out, "\nConflict at: %s\n", path.String())
fmt.Fprintf(out, "Left: %s\nRight: %s\n\n", left, right)
var choice rune
for {
fmt.Fprintln(out, "Enter 'l' to accept the left value, 'r' to accept the right value")
_, err := fmt.Fscanf(in, "%c\n", &choice)
d.PanicIfError(err)
switch choice {
case 'l', 'L':
return aType, a, true
case 'r', 'R':
return bType, b, true
}
}
}

220
cmd/noms/noms_merge_test.go Normal file
View File

@@ -0,0 +1,220 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"bytes"
"io/ioutil"
"os"
"testing"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/spec"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/clienttest"
"github.com/attic-labs/testify/assert"
"github.com/attic-labs/testify/suite"
)
type nomsMergeTestSuite struct {
clienttest.ClientTestSuite
}
func TestNomsMerge(t *testing.T) {
suite.Run(t, &nomsMergeTestSuite{})
}
func (s *nomsMergeTestSuite) TearDownTest() {
s.NoError(os.RemoveAll(s.LdbDir))
}
func (s *nomsMergeTestSuite) TestNomsMerge_Success() {
left, right := "left", "right"
p := s.setupMergeDataset(
"parent",
types.StructData{
"num": types.Number(42),
"str": types.String("foobar"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1)),
},
types.NewSet())
l := s.setupMergeDataset(
left,
types.StructData{
"num": types.Number(42),
"str": types.String("foobaz"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1)),
},
types.NewSet(p))
r := s.setupMergeDataset(
right,
types.StructData{
"num": types.Number(42),
"str": types.String("foobar"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1), types.Number(2), types.String("bar")),
},
types.NewSet(p))
expected := types.NewStruct("", types.StructData{
"num": types.Number(42),
"str": types.String("foobaz"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1), types.Number(2), types.String("bar")),
})
output := "output"
stdout, stderr, err := s.Run(main, []string{"merge", s.LdbDir, left, right, output})
if err == nil {
s.Equal("", stderr)
s.validateDataset(output, expected, l, r)
} else {
s.Fail("Run failed", "err: %v\nstdout: %s\nstderr: %s\n", err, stdout, stderr)
}
}
func (s *nomsMergeTestSuite) setupMergeDataset(name string, data types.StructData, p types.Set) types.Ref {
db, ds, _ := spec.GetDataset(spec.CreateValueSpecString("ldb", s.LdbDir, name))
defer db.Close()
ds, err := db.Commit(ds, types.NewStruct("", data), datas.CommitOptions{Parents: p})
s.NoError(err)
return ds.HeadRef()
}
func (s *nomsMergeTestSuite) validateDataset(name string, expected types.Struct, parents ...types.Value) {
db, ds, err := spec.GetDataset(spec.CreateValueSpecString("ldb", s.LdbDir, name))
if s.NoError(err) {
commit := ds.Head()
s.True(commit.Get(datas.ParentsField).Equals(types.NewSet(parents...)))
merged := ds.HeadValue()
s.True(expected.Equals(merged), "%s != %s", types.EncodedValue(expected), types.EncodedValue(merged))
}
defer db.Close()
}
func (s *nomsMergeTestSuite) TestNomsMerge_Left() {
left, right := "left", "right"
p := s.setupMergeDataset("parent", types.StructData{"num": types.Number(42)}, types.NewSet())
l := s.setupMergeDataset(left, types.StructData{"num": types.Number(43)}, types.NewSet(p))
r := s.setupMergeDataset(right, types.StructData{"num": types.Number(44)}, types.NewSet(p))
expected := types.NewStruct("", types.StructData{"num": types.Number(43)})
output := "output"
stdout, stderr, err := s.Run(main, []string{"merge", "--policy=l", s.LdbDir, left, right, output})
if err == nil {
s.Equal("", stderr)
s.validateDataset(output, expected, l, r)
} else {
s.Fail("Run failed", "err: %v\nstdout: %s\nstderr: %s\n", err, stdout, stderr)
}
}
func (s *nomsMergeTestSuite) TestNomsMerge_Right() {
left, right := "left", "right"
p := s.setupMergeDataset("parent", types.StructData{"num": types.Number(42)}, types.NewSet())
l := s.setupMergeDataset(left, types.StructData{"num": types.Number(43)}, types.NewSet(p))
r := s.setupMergeDataset(right, types.StructData{"num": types.Number(44)}, types.NewSet(p))
expected := types.NewStruct("", types.StructData{"num": types.Number(44)})
output := "output"
stdout, stderr, err := s.Run(main, []string{"merge", "--policy=r", s.LdbDir, left, right, output})
if err == nil {
s.Equal("", stderr)
s.validateDataset(output, expected, l, r)
} else {
s.Fail("Run failed", "err: %v\nstdout: %s\nstderr: %s\n", err, stdout, stderr)
}
}
func (s *nomsMergeTestSuite) TestNomsMerge_Conflict() {
left, right := "left", "right"
p := s.setupMergeDataset("parent", types.StructData{"num": types.Number(42)}, types.NewSet())
s.setupMergeDataset(left, types.StructData{"num": types.Number(43)}, types.NewSet(p))
s.setupMergeDataset(right, types.StructData{"num": types.Number(44)}, types.NewSet(p))
s.Panics(func() { s.MustRun(main, []string{"merge", s.LdbDir, left, right, "output"}) })
}
func (s *nomsMergeTestSuite) TestBadInput() {
sp := spec.CreateDatabaseSpecString("ldb", s.LdbDir)
l, r, o := "left", "right", "output"
type c struct {
args []string
err string
}
cases := []c{
{[]string{"foo"}, "error: Incorrect number of arguments\n"},
{[]string{"foo", "bar"}, "error: Incorrect number of arguments\n"},
{[]string{"foo", "bar", "baz"}, "error: Incorrect number of arguments\n"},
{[]string{"foo", "bar", "baz", "quux", "five"}, "error: Incorrect number of arguments\n"},
{[]string{sp, l + "!!", r, o}, "error: Invalid dataset " + l + "!!, must match [a-zA-Z0-9\\-_/]+\n"},
{[]string{sp, l + "2", r, o}, "error: Dataset " + l + "2 has no data\n"},
{[]string{sp, l, r + "2", o}, "error: Dataset " + r + "2 has no data\n"},
{[]string{sp, l, r, "!invalid"}, "error: Invalid dataset !invalid, must match [a-zA-Z0-9\\-_/]+\n"},
}
db, _ := spec.GetDatabase(sp)
prep := func(dsName string) {
ds := db.GetDataset(dsName)
db.CommitValue(ds, types.NewMap(types.String("foo"), types.String("bar")))
}
prep(l)
prep(r)
db.Close()
for _, c := range cases {
stdout, stderr, err := s.Run(main, append([]string{"merge"}, c.args...))
s.Empty(stdout, "Expected empty stdout for case: %#v", c.args)
if !s.NotNil(err, "Unexpected success for case: %#v\n", c.args) {
continue
}
if mainErr, ok := err.(clienttest.ExitError); ok {
s.Equal(1, mainErr.Code)
s.Equal(c.err, stderr, "Unexpected output for case: %#v\n", c.args)
} else {
s.Fail("Run() recovered non-error panic", "err: %#v\nstdout: %s\nstderr: %s\n", err, stdout, stderr)
}
}
}
func TestNomsMergeCliResolve(t *testing.T) {
type c struct {
input string
aChange, bChange types.DiffChangeType
aVal, bVal types.Value
expectedChange types.DiffChangeType
expected types.Value
success bool
}
cases := []c{
{"l\n", types.DiffChangeAdded, types.DiffChangeAdded, types.String("foo"), types.String("bar"), types.DiffChangeAdded, types.String("foo"), true},
{"r\n", types.DiffChangeAdded, types.DiffChangeAdded, types.String("foo"), types.String("bar"), types.DiffChangeAdded, types.String("bar"), true},
{"l\n", types.DiffChangeAdded, types.DiffChangeAdded, types.Number(7), types.String("bar"), types.DiffChangeAdded, types.Number(7), true},
{"r\n", types.DiffChangeModified, types.DiffChangeModified, types.Number(7), types.String("bar"), types.DiffChangeModified, types.String("bar"), true},
}
for _, c := range cases {
input := bytes.NewBufferString(c.input)
changeType, newVal, ok := cliResolve(input, ioutil.Discard, c.aChange, c.bChange, c.aVal, c.bVal, types.Path{})
if !c.success {
assert.False(t, ok)
} else if assert.True(t, ok) {
assert.Equal(t, c.expectedChange, changeType)
assert.True(t, c.expected.Equals(newVal))
}
}
}

View File

@@ -10,15 +10,23 @@ import (
var (
verbose bool
quiet bool
)
// RegisterVerboseFlags registers -v|--verbose flags for general usage
func RegisterVerboseFlags(flags *flag.FlagSet) {
flags.BoolVar(&verbose, "verbose", false, "show more")
flags.BoolVar(&verbose, "v", false, "")
flags.BoolVar(&quiet, "quiet", false, "show less")
flags.BoolVar(&quiet, "q", false, "")
}
// Verbose returns True if the verbose flag was set
func Verbose() bool {
return verbose
}
// Quiet returns True if the verbose flag was set
func Quiet() bool {
return quiet
}

View File

@@ -1 +0,0 @@
noms-merge

View File

@@ -1,159 +0,0 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"fmt"
"io"
"os"
"regexp"
"github.com/attic-labs/noms/go/config"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/merge"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/exit"
"github.com/attic-labs/noms/go/util/status"
"github.com/attic-labs/noms/go/util/verbose"
flag "github.com/juju/gnuflag"
)
var datasetRe = regexp.MustCompile("^" + datas.DatasetRe.String() + "$")
func main() {
if err := nomsMerge(); err != nil {
fmt.Println(err)
exit.Fail()
}
}
func nomsMerge() error {
outDSStr := flag.String("out-ds-name", "", "output dataset to write to - if empty, defaults to <right-ds-name>")
parentStr := flag.String("parent", "", "common ancestor of <left-ds-name> and <right-ds-name> (currently required; soon to be optional)")
quiet := flag.Bool("quiet", false, "silence progress output")
verbose.RegisterVerboseFlags(flag.CommandLine)
flag.Usage = usage
return d.Unwrap(d.Try(func() {
flag.Parse(false)
if flag.NArg() == 0 {
flag.Usage()
d.PanicIfTrue(true, "")
}
d.PanicIfTrue(flag.NArg() != 3, "Incorrect number of arguments\n")
d.PanicIfTrue(*parentStr == "", "--parent is required\n")
cfg := config.NewResolver()
db, err := cfg.GetDatabase(flag.Arg(0))
defer db.Close()
d.PanicIfError(err)
makeDS := func(dsName string) datas.Dataset {
d.PanicIfTrue(!datasetRe.MatchString(dsName), "Invalid dataset %s, must match %s\n", dsName, datas.DatasetRe.String())
return db.GetDataset(dsName)
}
leftDS := makeDS(flag.Arg(1))
rightDS := makeDS(flag.Arg(2))
parentDS := makeDS(*parentStr)
parent, ok := parentDS.MaybeHeadValue()
d.PanicIfTrue(!ok, "Dataset %s has no data\n", *parentStr)
left, ok := leftDS.MaybeHeadValue()
d.PanicIfTrue(!ok, "Dataset %s has no data\n", flag.Arg(1))
right, ok := rightDS.MaybeHeadValue()
d.PanicIfTrue(!ok, "Dataset %s has no data\n", flag.Arg(2))
outDS := rightDS
if *outDSStr != "" {
outDS = makeDS(*outDSStr)
}
pc := make(chan struct{}, 128)
go func() {
count := 0
for range pc {
if !*quiet {
count++
status.Printf("Applied %d changes...", count)
}
}
}()
resolve := func(aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
return cliResolve(os.Stdin, os.Stdout, aType, bType, a, b, path)
}
merged, err := merge.ThreeWay(left, right, parent, db, resolve, pc)
d.PanicIfError(err)
_, err = db.Commit(outDS, merged, datas.CommitOptions{
Parents: types.NewSet(leftDS.HeadRef(), rightDS.HeadRef()),
Meta: parentDS.Head().Get(datas.MetaField).(types.Struct),
})
d.PanicIfError(err)
if !*quiet {
status.Printf("Done")
status.Done()
}
}))
}
func cliResolve(in io.Reader, out io.Writer, aType, bType types.DiffChangeType, a, b types.Value, path types.Path) (change types.DiffChangeType, merged types.Value, ok bool) {
stringer := func(v types.Value) (s string, success bool) {
switch v := v.(type) {
case types.Bool, types.Number, types.String:
return fmt.Sprintf("%v", v), true
}
return "", false
}
left, lOk := stringer(a)
right, rOk := stringer(b)
if !lOk || !rOk {
return change, merged, false
}
// TODO: Handle removes as well.
fmt.Fprintf(out, "\nConflict at: %s\n", path.String())
fmt.Fprintf(out, "Left: %s\nRight: %s\n\n", left, right)
var choice rune
for {
fmt.Fprintln(out, "Enter 'l' to accept the left value, 'r' to accept the right value, or 'm' to mash them together")
_, err := fmt.Fscanf(in, "%c\n", &choice)
d.PanicIfError(err)
switch choice {
case 'l', 'L':
return aType, a, true
case 'r', 'R':
return bType, b, true
case 'm', 'M':
if !a.Type().Equals(b.Type()) {
fmt.Fprintf(out, "Sorry, can't merge a %s with a %s\n", a.Type().Describe(), b.Type().Describe())
return change, merged, false
}
switch a := a.(type) {
case types.Bool:
merged = types.Bool(bool(a) || bool(b.(types.Bool)))
case types.Number:
merged = types.Number(float64(a) + float64(b.(types.Number)))
case types.String:
merged = types.String(string(a) + string(b.(types.String)))
}
fmt.Fprintln(out, "Replacing with", types.EncodedValue(merged))
return aType, merged, true
}
}
}
func usage() {
fmt.Fprintf(os.Stderr, "Attempts to merge the two datasets in the provided database and commit the merge to either <right-ds-name> or another dataset of your choice.\n\n")
fmt.Fprintf(os.Stderr, "Usage: %s [--out-ds-name=<name>] [--parent=<name>] <db-spec> <left-ds-name> <right-ds-name>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " <db-spec> : database in which named datasets live\n")
fmt.Fprintf(os.Stderr, " <left-ds-name> : name of a dataset descending from <parent>\n")
fmt.Fprintf(os.Stderr, " <right-ds-name> : name of another dataset descending from <parent>\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n\n")
flag.PrintDefaults()
}

View File

@@ -1,152 +0,0 @@
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package main
import (
"bytes"
"io/ioutil"
"testing"
"github.com/attic-labs/noms/go/spec"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/clienttest"
"github.com/attic-labs/testify/suite"
)
func TestBasics(t *testing.T) {
suite.Run(t, &testSuite{})
}
type testSuite struct {
clienttest.ClientTestSuite
}
func (s *testSuite) TestWin() {
prep := func(name string, data types.StructData) {
db, ds, _ := spec.GetDataset(spec.CreateValueSpecString("ldb", s.LdbDir, name))
defer db.Close()
db.CommitValue(ds, types.NewStruct("", data))
}
p := "parent"
prep(p, types.StructData{
"num": types.Number(42),
"str": types.String("foobar"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1)),
})
l := "left"
prep(l, types.StructData{
"num": types.Number(42),
"str": types.String("foobaz"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1)),
})
r := "right"
prep(r, types.StructData{
"num": types.Number(42),
"str": types.String("foobar"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1), types.Number(2), types.String("bar")),
})
expected := types.NewStruct("", types.StructData{
"num": types.Number(42),
"str": types.String("foobaz"),
"lst": types.NewList(types.Number(1), types.String("foo")),
"map": types.NewMap(types.Number(1), types.String("foo"),
types.String("foo"), types.Number(1), types.Number(2), types.String("bar")),
})
var mainErr error
stdout, stderr, _ := s.Run(func() { mainErr = nomsMerge() }, []string{"--quiet=true", "--parent=" + p, s.LdbDir, l, r})
if s.NoError(mainErr, "%s", mainErr) {
s.Equal("", stdout)
s.Equal("", stderr)
db, ds, err := spec.GetDataset(spec.CreateValueSpecString("ldb", s.LdbDir, r))
if s.NoError(err) {
merged := ds.HeadValue()
s.True(expected.Equals(merged), "%s != %s", types.EncodedValue(expected), types.EncodedValue(merged))
}
defer db.Close()
}
}
func (s *testSuite) TestLose() {
sp := spec.CreateDatabaseSpecString("ldb", s.LdbDir)
p, l, r := "parent", "left", "right"
type c struct {
args []string
err string
}
cases := []c{
{[]string{"foo"}, "Incorrect number of arguments\n"},
{[]string{"foo", "bar"}, "Incorrect number of arguments\n"},
{[]string{"foo", "bar", "baz", "quux"}, "Incorrect number of arguments\n"},
{[]string{"foo", "bar", "baz"}, "--parent is required\n"},
{[]string{"--parent=" + p, sp, l + "!!", r}, "Invalid dataset " + l + "!!, must match [a-zA-Z0-9\\-_/]+\n"},
{[]string{"--parent=" + p, sp, l + "2", r}, "Dataset " + l + "2 has no data\n"},
{[]string{"--parent=" + p + "2", sp, l, r}, "Dataset " + p + "2 has no data\n"},
{[]string{"--parent=" + p, sp, l, r + "2"}, "Dataset " + r + "2 has no data\n"},
{[]string{"--parent=" + p, "--out-ds-name", "!invalid", sp, l, r}, "Invalid dataset !invalid, must match [a-zA-Z0-9\\-_/]+\n"},
}
db, _ := spec.GetDatabase(sp)
prep := func(dsName string) {
ds := db.GetDataset(dsName)
db.CommitValue(ds, types.NewMap(types.String("foo"), types.String("bar")))
}
prep(p)
prep(l)
prep(r)
db.Close()
for _, c := range cases {
var mainErr error
stdout, _, _ := s.Run(func() { mainErr = nomsMerge() }, c.args)
s.Empty(stdout, "Expected empty stdout for case: %#v", c.args)
if s.Error(mainErr) {
s.Equal(c.err, mainErr.Error(), "Unexpected output for case: %#v\n", c.args)
}
}
}
func (s *testSuite) TestResolve() {
type c struct {
input string
aChange, bChange types.DiffChangeType
aVal, bVal types.Value
expectedChange types.DiffChangeType
expected types.Value
success bool
}
cases := []c{
{"l\n", types.DiffChangeAdded, types.DiffChangeAdded, types.String("foo"), types.String("bar"), types.DiffChangeAdded, types.String("foo"), true},
{"r\n", types.DiffChangeAdded, types.DiffChangeAdded, types.String("foo"), types.String("bar"), types.DiffChangeAdded, types.String("bar"), true},
{"m\n", types.DiffChangeAdded, types.DiffChangeAdded, types.String("foo"), types.String("bar"), types.DiffChangeAdded, types.String("foobar"), true},
{"l\n", types.DiffChangeAdded, types.DiffChangeAdded, types.Number(7), types.String("bar"), types.DiffChangeAdded, types.Number(7), true},
{"r\n", types.DiffChangeModified, types.DiffChangeModified, types.Number(7), types.String("bar"), types.DiffChangeModified, types.String("bar"), true},
{"m\n", types.DiffChangeModified, types.DiffChangeModified, types.Number(7), types.String("bar"), types.DiffChangeModified, nil, false},
}
for _, c := range cases {
input := bytes.NewBufferString(c.input)
changeType, newVal, ok := cliResolve(input, ioutil.Discard, c.aChange, c.bChange, c.aVal, c.bVal, types.Path{})
if !c.success {
s.False(ok)
} else if s.True(ok) {
s.Equal(c.expectedChange, changeType)
s.True(c.expected.Equals(newVal))
}
}
}