diff options
Diffstat (limited to 'Godeps/_workspace/src/github.com/obscuren/qml/qml_test.go')
-rw-r--r-- | Godeps/_workspace/src/github.com/obscuren/qml/qml_test.go | 1436 |
1 files changed, 1436 insertions, 0 deletions
diff --git a/Godeps/_workspace/src/github.com/obscuren/qml/qml_test.go b/Godeps/_workspace/src/github.com/obscuren/qml/qml_test.go new file mode 100644 index 000000000..2db23cd23 --- /dev/null +++ b/Godeps/_workspace/src/github.com/obscuren/qml/qml_test.go @@ -0,0 +1,1436 @@ +package qml_test + +import ( + "encoding/base64" + "flag" + "fmt" + "image" + "image/color" + "io/ioutil" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "testing" + "time" + + . "gopkg.in/check.v1" + "gopkg.in/qml.v1" + "gopkg.in/qml.v1/cpptest" + "gopkg.in/qml.v1/gl/2.0" + "path/filepath" +) + +func init() { qml.SetupTesting() } + +func Test(t *testing.T) { TestingT(t) } + +type S struct { + engine *qml.Engine + context *qml.Context +} + +var _ = Suite(&S{}) + +func (s *S) SetUpTest(c *C) { + qml.SetLogger(c) + qml.CollectStats(true) + qml.ResetStats() + + stats := qml.Stats() + if stats.EnginesAlive > 0 || stats.ValuesAlive > 0 || stats.ConnectionsAlive > 0 { + panic(fmt.Sprintf("Test started with values alive: %#v\n", stats)) + } + + s.engine = qml.NewEngine() + s.context = s.engine.Context() +} + +func (s *S) TearDownTest(c *C) { + s.engine.Destroy() + + retries := 30 // Three seconds top. + for { + // Do not call qml.Flush here. It creates a nested event loop + // that attempts to process the deferred object deletes and cannot, + // because deferred deletes are only processed at the same loop level. + // So it *reposts* the deferred deletion event, in practice *preventing* + // these objects from being deleted. + runtime.GC() + stats := qml.Stats() + if stats.EnginesAlive == 0 && stats.ValuesAlive == 0 && stats.ConnectionsAlive == 0 { + break + } + if retries == 0 { + panic(fmt.Sprintf("there are values alive:\n%#v\n", stats)) + } + retries-- + time.Sleep(100 * time.Millisecond) + if retries%10 == 0 { + c.Logf("There are still objects alive; waiting for them to die: %#v\n", stats) + } + } + + qml.SetLogger(nil) +} + +type GoRect struct { + PaintCount int +} + +func (r *GoRect) Paint(p *qml.Painter) { + r.PaintCount++ + + obj := p.Object() + + gl := GL.API(p) + + width := float32(obj.Int("width")) + height := float32(obj.Int("height")) + + gl.Color3f(1.0, 0.0, 0.0) + gl.Begin(GL.QUADS) + gl.Vertex2f(0, 0) + gl.Vertex2f(width, 0) + gl.Vertex2f(width, height) + gl.Vertex2f(0, height) + gl.End() +} + +type GoType struct { + private bool // Besides being private, also adds a gap in the reflect field index. + + StringValue string + StringAddrValue *string + BoolValue bool + IntValue int + Int64Value int64 + Int32Value int32 + Uint32Value uint32 + Float64Value float64 + Float32Value float32 + AnyValue interface{} + ObjectValue qml.Object + ColorValue color.RGBA + IntsValue []int + ObjectsValue []qml.Object + MapValue map[string]interface{} + + SetterStringValue string + SetterObjectsValue []qml.Object + + setterStringValueChanged int + setterStringValueSet string + setterObjectsValueChanged int + setterObjectsValueSet []qml.Object + + getterStringValue string + getterStringValueChanged int + + // The object representing this value, on custom type tests. + object qml.Object +} + +// Force a gap in the reflect method index and ensure the handling +// of private methods is being done properly. +func (ts *GoType) privateMethod() {} + +func (ts *GoType) StringMethod() string { + return ts.StringValue +} + +func (ts *GoType) SetSetterStringValue(s string) { + ts.setterStringValueChanged++ + ts.setterStringValueSet = s +} + +func (ts *GoType) SetSetterObjectsValue(v []qml.Object) { + ts.setterObjectsValueChanged++ + ts.setterObjectsValueSet = v +} + +func (ts *GoType) GetterStringValue() string { + return ts.getterStringValue +} + +func (ts *GoType) SetGetterStringValue(s string) { + ts.getterStringValueChanged++ + ts.getterStringValue = s +} + +func (ts *GoType) SetMapValue(m map[string]interface{}) { + ts.MapValue = m +} + +func (ts *GoType) Mod(dividend, divisor int) (int, error) { + if divisor == 0 { + return 0, fmt.Errorf("<division by zero>") + } + return dividend % divisor, nil +} + +func (ts *GoType) ChangeString(new string) (old string) { + old = ts.StringValue + ts.StringValue = new + return +} + +func (ts *GoType) NotifyStringChanged() { + qml.Changed(ts, &ts.StringValue) +} + +func (ts *GoType) IncrementInt() { + ts.IntValue++ +} + +func (s *S) TestEngineDestroyedUse(c *C) { + s.engine.Destroy() + s.engine.Destroy() + c.Assert(s.engine.Context, PanicMatches, "engine already destroyed") +} + +var same = "<same>" + +var getSetTests = []struct{ set, get interface{} }{ + {"value", same}, + {true, same}, + {false, same}, + {int(42), same}, + {int32(42), int(42)}, + {int64(42), same}, + {uint32(42), same}, + {uint64(42), same}, + {float64(42), same}, + {float32(42), same}, + {new(GoType), same}, + {nil, same}, + {42, same}, +} + +func (s *S) TestContextGetSet(c *C) { + for i, t := range getSetTests { + want := t.get + if t.get == same { + want = t.set + } + s.context.SetVar("key", t.set) + c.Assert(s.context.Var("key"), Equals, want, + Commentf("entry %d is {%v (%T), %v (%T)}", i, t.set, t.set, t.get, t.get)) + } +} + +func (s *S) TestContextGetMissing(c *C) { + c.Assert(s.context.Var("missing"), Equals, nil) +} + +func (s *S) TestContextSetVars(c *C) { + component, err := s.engine.LoadString("file.qml", "import QtQuick 2.0\nItem { width: 42 }") + c.Assert(err, IsNil) + root := component.Create(nil) + + vars := GoType{ + StringValue: "<content>", + BoolValue: true, + IntValue: 42, + Int64Value: 42, + Int32Value: 42, + Float64Value: 4.2, + Float32Value: 4.2, + AnyValue: nil, + ObjectValue: root, + } + s.context.SetVars(&vars) + + c.Assert(s.context.Var("stringValue"), Equals, "<content>") + c.Assert(s.context.Var("boolValue"), Equals, true) + c.Assert(s.context.Var("intValue"), Equals, 42) + c.Assert(s.context.Var("int64Value"), Equals, int64(42)) + c.Assert(s.context.Var("int32Value"), Equals, 42) + c.Assert(s.context.Var("float64Value"), Equals, float64(4.2)) + c.Assert(s.context.Var("float32Value"), Equals, float32(4.2)) + c.Assert(s.context.Var("anyValue"), Equals, nil) + + vars.AnyValue = 42 + c.Assert(s.context.Var("anyValue"), Equals, 42) + + c.Assert(s.context.Var("objectValue").(qml.Object).Int("width"), Equals, 42) +} + +func (s *S) TestComponentSetDataError(c *C) { + _, err := s.engine.LoadString("file.qml", "Item{}") + c.Assert(err, ErrorMatches, "file:.*/file.qml:1 Item is not a type") +} + +func (s *S) TestComponentCreateWindow(c *C) { + data := ` + import QtQuick 2.0 + Item { width: 300; height: 200; } + ` + component, err := s.engine.LoadString("file.qml", data) + c.Assert(err, IsNil) + + // TODO How to test this more effectively? + window := component.CreateWindow(nil) + window.Show() + + // Just a smoke test, as there isn't much to assert. + c.Assert(window.PlatformId(), Not(Equals), uintptr(0)) + + // Qt doesn't hide the Window if we call it too quickly. :-( + time.Sleep(100 * time.Millisecond) + window.Hide() +} + +func (s *S) TestContextSpawn(c *C) { + context1 := s.engine.Context() + context2 := context1.Spawn() + + context1.SetVar("mystr", "context1") + context2.SetVar("mystr", "context2") + + data := ` + import QtQuick 2.0 + Item { property var s: mystr } + ` + component, err := s.engine.LoadString("file.qml", data) + c.Assert(err, IsNil) + + obj1 := component.Create(context1) + obj2 := component.Create(context2) + + c.Assert(obj1.String("s"), Equals, "context1") + c.Assert(obj2.String("s"), Equals, "context2") +} + +func (s *S) TestReadVoidAddrProperty(c *C) { + obj := cpptest.NewTestType(s.engine) + addr := obj.Property("voidAddr").(uintptr) + c.Assert(addr, Equals, uintptr(42)) +} + +func (s *S) TestRegisterConverterPlainObject(c *C) { + qml.RegisterConverter("PlainTestType", func(engine *qml.Engine, obj qml.Object) interface{} { + c.Check(engine, Equals, s.engine) + c.Check(obj.String("plainType"), Matches, "(const )?PlainTestType[&*]?") + c.Check(obj.Property("plainAddr"), FitsTypeOf, uintptr(0)) + c.Check(cpptest.PlainTestTypeN(obj), Equals, 42) + return "<converted>" + }) + obj := cpptest.NewTestType(s.engine) + defer obj.Destroy() + + var calls int + obj.On("plainEmittedCpy", func(s string) { + c.Check(s, Equals, "<converted>") + calls++ + }) + obj.On("plainEmittedRef", func(s string) { + c.Check(s, Equals, "<converted>") + calls++ + }) + obj.On("plainEmittedPtr", func(s string) { + c.Check(s, Equals, "<converted>") + calls++ + }) + obj.Call("emitPlain") + c.Assert(calls, Equals, 3) +} + +func (s *S) TestIssue84(c *C) { + // Regression test for issue #84 (QTBUG-41193). + data := ` + import QtQuick 2.0 + Item { + id: item + property string s1: "<before>" + property string s2: "<after>" + states: State { + name: "after"; + PropertyChanges { target: item; s1: s2 } + } + Component.onCompleted: state = "after" + } + ` + filename := filepath.Join(c.MkDir(), "file.qml") + err := ioutil.WriteFile(filename, []byte(data), 0644) + c.Assert(err, IsNil) + + component, err := s.engine.LoadString(filename, data) + c.Assert(err, IsNil) + + root := component.Create(nil) + defer root.Destroy() + + c.Assert(root.String("s1"), Equals, "<after>") +} + +func (s *S) TestResources(c *C) { + var rp qml.ResourcesPacker + rp.Add("sub/path/Foo.qml", []byte("import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Foo>') }")) + rp.AddString("sub/path/Bar.qml", "import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Bar>') }") + rp.AddString("/sub/Main.qml", "import QtQuick 2.0\nimport \"./path\"\nItem {\nFoo{}\nBar{}\n}") + + r := rp.Pack() + qml.LoadResources(r) + testResourcesLoaded(c, true) + qml.UnloadResources(r) + testResourcesLoaded(c, false) + + data := r.Bytes() + + rb, err := qml.ParseResources(data) + c.Assert(err, IsNil) + qml.LoadResources(rb) + testResourcesLoaded(c, true) + qml.UnloadResources(rb) + testResourcesLoaded(c, false) + + rs, err := qml.ParseResourcesString(string(data)) + c.Assert(err, IsNil) + qml.LoadResources(rs) + testResourcesLoaded(c, true) + qml.UnloadResources(rs) + testResourcesLoaded(c, false) +} + +func testResourcesLoaded(c *C, loaded bool) { + engine := qml.NewEngine() + defer engine.Destroy() + component, err := engine.LoadFile("qrc:///sub/Main.qml") + if loaded { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, "qrc:///sub/Main.qml:-1 File not found") + return + } + root := component.Create(nil) + defer root.Destroy() + c.Assert(c.GetTestLog(), Matches, "(?s).*(<Foo>.*<Bar>|<Bar>.*<Foo>).*") +} + +func (s *S) TestResourcesIssue107(c *C) { + var rp qml.ResourcesPacker + + rp.Add("a/Foo.qml", []byte("import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Foo>') }")) + rp.Add("b/Bar.qml", []byte("import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Bar>') }")) + rp.Add("c/Baz.qml", []byte("import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Baz>') }")) + rp.Add("d/Buz.qml", []byte("import QtQuick 2.0\nItem { Component.onCompleted: console.log('<Buz>') }")) + + r := rp.Pack() + qml.LoadResources(r) + + for _, name := range []string{"a/Foo", "b/Bar", "c/Baz", "d/Buz"} { + component, err := s.engine.LoadFile("qrc:///" + name + ".qml") + c.Assert(err, IsNil) + root := component.Create(nil) + defer root.Destroy() + } + c.Assert(c.GetTestLog(), Matches, "(?s).*<Foo>.*<Bar>.*<Baz>.*<Buz>.*") +} + +type TestData struct { + *C + engine *qml.Engine + context *qml.Context + component qml.Object + root qml.Object + value *GoType + createdValue []*GoType + createdRect []*GoRect + createdSingleton []*GoType +} + +var tests = []struct { + Summary string + Value GoType + Rect GoRect + + Init func(d *TestData) + + // The QML provided is run with the initial state above, and + // then checks are made to ensure the provided state is found. + QML string + QMLLog string + QMLValue GoType + + // The function provided is run with the post-QML state above, + // and then checks are made to ensure the provided state is found. + Done func(c *TestData) + DoneLog string + DoneValue GoType +}{ + { + Summary: "Read a context variable and its fields", + Value: GoType{StringValue: "<content>", IntValue: 42}, + QML: ` + Item { + Component.onCompleted: { + console.log("String is", value.stringValue) + console.log("Int is", value.intValue) + console.log("Any is", value.anyValue) + } + } + `, + QMLLog: "String is <content>.*Int is 42.*Any is undefined", + }, + { + Summary: "Read a nested field via a value (not pointer) in an interface", + Value: GoType{AnyValue: struct{ StringValue string }{"<content>"}}, + QML: `Item { Component.onCompleted: console.log("String is", value.anyValue.stringValue) }`, + QMLLog: "String is <content>", + }, + { + Summary: "Read a native property", + QML: `Item { width: 123 }`, + Done: func(c *TestData) { c.Check(c.root.Int("width"), Equals, 123) }, + }, + { + Summary: "Read object properties", + QML: ` + Item { + property bool boolp: true + property int intp: 1 + property var int64p: 4294967296 + property real float32p: 1.1 + property double float64p: 1.1 + property string stringp: "<content>" + property var objectp: Rectangle { width: 123 } + property var nilp: null + } + `, + Done: func(c *TestData) { + obj := c.root + c.Check(obj.Bool("boolp"), Equals, true) + c.Check(obj.Int("intp"), Equals, 1) + c.Check(obj.Int64("intp"), Equals, int64(1)) + c.Check(obj.Int64("int64p"), Equals, int64(4294967296)) + c.Check(obj.Float64("intp"), Equals, float64(1)) + c.Check(obj.Float64("int64p"), Equals, float64(4294967296)) + c.Check(obj.Float64("float32p"), Equals, float64(1.1)) + c.Check(obj.Float64("float64p"), Equals, float64(1.1)) + c.Check(obj.String("stringp"), Equals, "<content>") + c.Check(obj.Object("objectp").Int("width"), Equals, 123) + c.Check(obj.Property("nilp"), Equals, nil) + + c.Check(func() { obj.Bool("intp") }, Panics, `value of property "intp" is not a bool: 1`) + c.Check(func() { obj.Int("boolp") }, Panics, `value of property "boolp" cannot be represented as an int: true`) + c.Check(func() { obj.Int64("boolp") }, Panics, `value of property "boolp" cannot be represented as an int64: true`) + c.Check(func() { obj.Float64("boolp") }, Panics, `value of property "boolp" cannot be represented as a float64: true`) + c.Check(func() { obj.String("boolp") }, Panics, `value of property "boolp" is not a string: true`) + c.Check(func() { obj.Object("boolp") }, Panics, `value of property "boolp" is not a QML object: true`) + c.Check(func() { obj.Property("missing") }, Panics, `object does not have a "missing" property`) + }, + }, + { + Summary: "Lowercasing of object properties", + Init: func(c *TestData) { + obj := struct{ THE, THEName, Name, N string }{"<a>", "<b>", "<c>", "<d>"} + c.context.SetVar("obj", &obj) + }, + QML: `Item { Component.onCompleted: console.log("Names are", obj.the, obj.theName, obj.name, obj.n) }`, + QMLLog: "Names are <a> <b> <c> <d>", + }, + { + Summary: "No access to private fields", + Value: GoType{private: true}, + QML: `Item { Component.onCompleted: console.log("Private is", value.private); }`, + QMLLog: "Private is undefined", + }, + { + Summary: "Set a custom property", + QML: ` + Item { + property var obj: null + + onObjChanged: console.log("String is", obj.stringValue) + onWidthChanged: console.log("Width is", width) + onHeightChanged: console.log("Height is", height) + } + `, + Done: func(c *TestData) { + value := GoType{StringValue: "<content>"} + c.root.Set("obj", &value) + c.root.Set("width", 300) + c.root.Set("height", 200) + }, + DoneLog: "String is <content>.*Width is 300.*Height is 200", + }, + { + Summary: "Read and set a QUrl property", + QML: `import QtWebKit 3.0; WebView {}`, + Done: func(c *TestData) { + c.Check(c.root.String("url"), Equals, "") + url := "http://localhost:54321" + c.root.Set("url", url) + c.Check(c.root.String("url"), Equals, url) + }, + }, + { + Summary: "Read and set a QColor property", + QML: `Text{ color: Qt.rgba(1/16, 1/8, 1/4, 1/2); function hasColor(c) { return Qt.colorEqual(color, c) }}`, + Done: func(c *TestData) { + c.Assert(c.root.Color("color"), Equals, color.RGBA{256 / 16, 256 / 8, 256 / 4, 256 / 2}) + c.root.Set("color", color.RGBA{256 / 2, 256 / 4, 256 / 8, 256 / 16}) + c.Assert(c.root.Call("hasColor", color.RGBA{256 / 2, 256 / 4, 256 / 8, 256 / 16}), Equals, true) + }, + }, + { + Summary: "Read and set a QColor property from a Go field", + Init: func(c *TestData) { c.value.ColorValue = color.RGBA{256 / 16, 256 / 8, 256 / 4, 256 / 2} }, + QML: `Text{ property var c: value.colorValue; Component.onCompleted: { console.log(value.colorValue); } }`, + Done: func(c *TestData) { + c.Assert(c.root.Color("c"), Equals, color.RGBA{256 / 16, 256 / 8, 256 / 4, 256 / 2}) + }, + }, + { + Summary: "Read a QQmlListProperty property into a Go slice", + QML: ` + Item { + states: [ + State { id: on; name: "on" }, + State { id: off; name: "off" } + ] + } + `, + Done: func(c *TestData) { + var states []qml.Object + c.root.List("states").Convert(&states) + c.Assert(states[0].String("name"), Equals, "on") + c.Assert(states[1].String("name"), Equals, "off") + c.Assert(len(states), Equals, 2) + c.Assert(c.root.Property("states").(*qml.List).Len(), Equals, 2) + }, + }, + { + Summary: "Read a QQmlListReference property into a Go slice", + QML: ` + Item { + property list<State> mystates: [ + State { id: on; name: "on" }, + State { id: off; name: "off" } + ] + Component.onCompleted: value.objectsValue = mystates + } + `, + Done: func(c *TestData) { + var states []qml.Object + c.root.List("mystates").Convert(&states) + c.Assert(states[0].String("name"), Equals, "on") + c.Assert(states[1].String("name"), Equals, "off") + c.Assert(len(states), Equals, 2) + c.Assert(len(c.value.ObjectsValue), Equals, 2) + }, + }, + { + Summary: "Read a QVariantList property into a Go slice", + QML: ` + Item { + State { id: on; name: "on" } + State { id: off; name: "off" } + property var mystates: [on, off] + } + `, + Done: func(c *TestData) { + var states []qml.Object + c.root.List("mystates").Convert(&states) + c.Assert(states[0].String("name"), Equals, "on") + c.Assert(states[1].String("name"), Equals, "off") + c.Assert(len(states), Equals, 2) + }, + }, + { + Summary: "Set a Go slice property", + QML: `Item { Component.onCompleted: value.intsValue = [1, 2, 3.5] }`, + QMLValue: GoType{IntsValue: []int{1, 2, 3}}, + }, + { + Summary: "Set a Go slice property with objects", + QML: ` + Item { + State { id: on; name: "on" } + State { id: off; name: "off" } + Component.onCompleted: value.objectsValue = [on, off] + } + `, + Done: func(c *TestData) { + c.Assert(c.value.ObjectsValue[0].String("name"), Equals, "on") + c.Assert(c.value.ObjectsValue[1].String("name"), Equals, "off") + c.Assert(len(c.value.ObjectsValue), Equals, 2) + }, + }, + { + Summary: "Call a method with a JSON object (issue #48)", + QML: `Item { Component.onCompleted: value.setMapValue({a: 1, b: 2}) }`, + QMLValue: GoType{MapValue: map[string]interface{}{"a": 1, "b": 2}}, + }, + { + Summary: "Read a map from a QML property", + QML: `Item { property var m: {"a": 1, "b": 2} }`, + Done: func(c *TestData) { + var m1 map[string]interface{} + var m2 map[string]int + m := c.root.Map("m") + m.Convert(&m1) + m.Convert(&m2) + c.Assert(m1, DeepEquals, map[string]interface{}{"a": 1, "b": 2}) + c.Assert(m2, DeepEquals, map[string]int{"a": 1, "b": 2}) + c.Assert(m.Len(), Equals, 2) + }, + }, + { + Summary: "Identical values remain identical when possible", + Init: func(c *TestData) { + c.context.SetVar("a", c.value) + c.context.SetVar("b", c.value) + }, + QML: `Item { Component.onCompleted: console.log('Identical:', a === b); }`, + QMLLog: "Identical: true", + }, + { + Summary: "Object finding via ObjectByName", + QML: `Item { Item { objectName: "subitem"; property string s: "<found>" } }`, + Done: func(c *TestData) { + obj := c.root.ObjectByName("subitem") + c.Check(obj.String("s"), Equals, "<found>") + c.Check(func() { c.root.ObjectByName("foo") }, Panics, `cannot find descendant with objectName == "foo"`) + }, + }, + { + Summary: "Object finding via ObjectByName on GoType", + QML: `Item { GoType { objectName: "subitem"; property string s: "<found>" } }`, + Done: func(c *TestData) { + obj := c.root.ObjectByName("subitem") + c.Check(obj.String("s"), Equals, "<found>") + c.Check(func() { c.root.ObjectByName("foo") }, Panics, `cannot find descendant with objectName == "foo"`) + }, + }, + { + Summary: "Register Go type", + QML: `GoType { objectName: "test"; Component.onCompleted: console.log("String is", stringValue) }`, + QMLLog: "String is <initial>", + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].object.String("objectName"), Equals, "test") + }, + }, + { + Summary: "Register Go type with an explicit name", + QML: `NamedGoType { objectName: "test"; Component.onCompleted: console.log("String is", stringValue) }`, + QMLLog: "String is <initial>", + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].object.String("objectName"), Equals, "test") + }, + }, + { + Summary: "Write Go type property", + QML: `GoType { stringValue: "<content>"; intValue: 300 }`, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].StringValue, Equals, "<content>") + c.Assert(c.createdValue[0].IntValue, Equals, 300) + }, + }, + { + Summary: "Write Go type property that has a setter", + QML: `GoType { setterStringValue: "<content>" }`, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].SetterStringValue, Equals, "") + c.Assert(c.createdValue[0].setterStringValueChanged, Equals, 1) + c.Assert(c.createdValue[0].setterStringValueSet, Equals, "<content>") + }, + }, + { + Summary: "Write Go type property that has a setter and a getter", + QML: ` + GoType { + getterStringValue: "<content>" + Component.onCompleted: console.log("Getter returned", getterStringValue) + } + `, + QMLLog: `Getter returned <content>`, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].getterStringValue, Equals, "<content>") + c.Assert(c.createdValue[0].getterStringValueChanged, Equals, 1) + }, + }, + { + Summary: "Write an inline object list to a Go type property", + QML: ` + GoType { + objectsValue: [State{ name: "on" }, State{ name: "off" }] + Component.onCompleted: { + console.log("Length:", objectsValue.length) + console.log("Name:", objectsValue[0].name) + } + } + `, + QMLLog: "Length: 2.*Name: on", + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].ObjectsValue[0].String("name"), Equals, "on") + c.Assert(c.createdValue[0].ObjectsValue[1].String("name"), Equals, "off") + c.Assert(c.createdValue[0].ObjectsValue, HasLen, 2) + }, + }, + { + Summary: "Write an inline object list to a Go type property that has a setter", + QML: `GoType { setterObjectsValue: [State{ name: "on" }, State{ name: "off" }] }`, + Done: func(c *TestData) { + // Note that the setter is not actually updating the field value, for testing purposes. + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].SetterObjectsValue, IsNil) + c.Assert(c.createdValue[0].setterObjectsValueChanged, Equals, 2) + c.Assert(c.createdValue[0].setterObjectsValueSet, HasLen, 1) + c.Assert(c.createdValue[0].setterObjectsValueSet[0].String("name"), Equals, "off") + }, + }, + { + Summary: "Clear an object list in a Go type property", + QML: ` + GoType { + objectsValue: [State{ name: "on" }, State{ name: "off" }] + Component.onCompleted: objectsValue = [] + } + `, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].ObjectsValue, HasLen, 0) + }, + }, + { + Summary: "Clear an object list in a Go type property that has a setter", + Value: GoType{SetterObjectsValue: []qml.Object{nil, nil}}, + QML: ` + GoType { + objectsValue: [State{ name: "on" }, State{ name: "off" }] + function clear() { setterObjectsValue = [] } + } + `, + Done: func(c *TestData) { + // Note that the setter is not actually updating the field value, for testing purposes. + c.Assert(c.createdValue, HasLen, 1) + c.createdValue[0].SetterObjectsValue = c.createdValue[0].ObjectsValue + + c.createdValue[0].object.Call("clear") + + c.Assert(c.createdValue[0].SetterObjectsValue, HasLen, 2) + c.Assert(c.createdValue[0].setterObjectsValueChanged, Equals, 1) + c.Assert(c.createdValue[0].setterObjectsValueSet, DeepEquals, []qml.Object{}) + c.Assert(&c.createdValue[0].SetterObjectsValue[0], Equals, &c.createdValue[0].ObjectsValue[0]) + }, + }, + { + Summary: "Access underlying Go value with Interface", + QML: `GoType { stringValue: "<content>" }`, + Done: func(c *TestData) { + c.Assert(c.root.Interface().(*GoType).StringValue, Equals, "<content>") + c.Assert(c.context.Interface, Panics, "QML object is not backed by a Go value") + }, + }, + { + Summary: "Notification signals on custom Go type", + QML: ` + GoType { + id: custom + stringValue: "<old>" + onStringValueChanged: if (custom.stringValue != "<newest>") { custom.stringValue = "<newest>" } + Component.onCompleted: custom.stringValue = "<new>" + } + `, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + c.Assert(c.createdValue[0].StringValue, Equals, "<newest>") + }, + }, + { + Summary: "Singleton type registration", + QML: `Item { Component.onCompleted: console.log("String is", GoSingleton.stringValue) }`, + QMLLog: "String is <initial>", + }, + { + Summary: "qml.Changed on unknown value is okay", + Value: GoType{StringValue: "<old>"}, + Init: func(c *TestData) { + value := &GoType{} + qml.Changed(&value, &value.StringValue) + }, + QML: `Item{}`, + }, + { + Summary: "qml.Changed triggers a QML slot", + QML: ` + GoType { + stringValue: "<old>" + onStringValueChanged: console.log("String is", stringValue) + onStringAddrValueChanged: console.log("String at addr is", stringAddrValue) + } + `, + QMLLog: "!String is", + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + value := c.createdValue[0] + s := "<new at addr>" + value.StringValue = "<new>" + value.StringAddrValue = &s + qml.Changed(value, &value.StringValue) + qml.Changed(value, &value.StringAddrValue) + }, + DoneLog: "String is <new>.*String at addr is <new at addr>", + }, + { + Summary: "qml.Changed must not trigger on the wrong field", + QML: ` + GoType { + stringValue: "<old>" + onStringValueChanged: console.log("String is", stringValue) + } + `, + Done: func(c *TestData) { + c.Assert(c.createdValue, HasLen, 1) + value := c.createdValue[0] + value.StringValue = "<new>" + qml.Changed(value, &value.IntValue) + }, + DoneLog: "!String is", + }, + { + Summary: "qml.Changed updates bindings", + Value: GoType{StringValue: "<old>"}, + QML: `Item { property string s: "String is " + value.stringValue }`, + Done: func(c *TestData) { + c.value.StringValue = "<new>" + qml.Changed(c.value, &c.value.StringValue) + c.Check(c.root.String("s"), Equals, "String is <new>") + }, + }, + { + Summary: "Call a Go method without arguments or result", + Value: GoType{IntValue: 42}, + QML: `Item { Component.onCompleted: console.log("Undefined is", value.incrementInt()); }`, + QMLLog: "Undefined is undefined", + QMLValue: GoType{IntValue: 43}, + }, + { + Summary: "Call a Go method with one argument and one result", + Value: GoType{StringValue: "<old>"}, + QML: `Item { Component.onCompleted: console.log("String was", value.changeString("<new>")); }`, + QMLLog: "String was <old>", + QMLValue: GoType{StringValue: "<new>"}, + }, + { + Summary: "Call a Go method with multiple results", + QML: ` + Item { + Component.onCompleted: { + var r = value.mod(42, 4); + console.log("mod is", r[0], "and err is", r[1]); + } + } + `, + QMLLog: `mod is 2 and err is undefined`, + }, + { + Summary: "Call a Go method that returns an error", + QML: ` + Item { + Component.onCompleted: { + var r = value.mod(0, 0); + console.log("err is", r[1].error()); + } + } + `, + QMLLog: `err is <division by zero>`, + }, + { + Summary: "Call a Go method that recurses back into the GUI thread", + QML: ` + Item { + Connections { + target: value + onStringValueChanged: console.log("Notification arrived") + } + Component.onCompleted: { + value.notifyStringChanged() + } + } + `, + QMLLog: "Notification arrived", + }, + { + Summary: "Connect a QML signal to a Go method", + Value: GoType{StringValue: "<old>"}, + QML: ` + Item { + id: item + signal testSignal(string s) + Component.onCompleted: { + item.testSignal.connect(value.changeString) + item.testSignal("<new>") + } + } + `, + QMLValue: GoType{StringValue: "<new>"}, + }, + { + Summary: "Call a QML method with no result or parameters from Go", + QML: `Item { function f() { console.log("f was called"); } }`, + Done: func(c *TestData) { c.Check(c.root.Call("f"), IsNil) }, + DoneLog: "f was called", + }, + { + Summary: "Call a QML method with result and parameters from Go", + QML: `Item { function add(a, b) { return a+b; } }`, + Done: func(c *TestData) { c.Check(c.root.Call("add", 1, 2.1), Equals, float64(3.1)) }, + }, + { + Summary: "Call a QML method with a custom type", + Value: GoType{StringValue: "<content>"}, + QML: `Item { function log(value) { console.log("String is", value.stringValue) } }`, + Done: func(c *TestData) { c.root.Call("log", c.value) }, + DoneLog: "String is <content>", + }, + { + Summary: "Call a QML method that returns a QML object", + QML: ` + Item { + property var custom: Rectangle { width: 300; } + function f() { return custom } + } + `, + Done: func(c *TestData) { + c.Check(c.root.Call("f").(qml.Object).Int("width"), Equals, 300) + }, + }, + { + Summary: "Call a QML method that holds a custom type past the return point", + QML: ` + Item { + property var held + function hold(v) { held = v; gc(); gc(); } + function log() { console.log("String is", held.stringValue) } + }`, + Done: func(c *TestData) { + value := GoType{StringValue: "<content>"} + stats := qml.Stats() + c.root.Call("hold", &value) + c.Check(qml.Stats().ValuesAlive, Equals, stats.ValuesAlive+1) + c.root.Call("log") + c.root.Call("hold", nil) + c.Check(qml.Stats().ValuesAlive, Equals, stats.ValuesAlive) + }, + DoneLog: "String is <content>", + }, + { + Summary: "Call a non-existent QML method", + QML: `Item {}`, + Done: func(c *TestData) { + c.Check(func() { c.root.Call("add", 1, 2) }, Panics, `object does not expose a method "add"`) + }, + }, + { + Summary: "Ensure URL of provided file is correct by loading a local file", + Init: func(c *TestData) { + data, err := base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==") + c.Assert(err, IsNil) + err = ioutil.WriteFile("test.gif", data, 0644) + c.Check(err, IsNil) + }, + QML: `Image { source: "test.gif"; Component.onCompleted: console.log("Ready:", status == Image.Ready) }`, + QMLLog: "Ready: true", + Done: func(c *TestData) { os.Remove("test.gif") }, + }, + { + Summary: "Create window with non-window root object", + QML: `Rectangle { width: 300; height: 200; function inc(x) { return x+1 } }`, + Done: func(c *TestData) { + win := c.component.CreateWindow(nil) + root := win.Root() + c.Check(root.Int("width"), Equals, 300) + c.Check(root.Int("height"), Equals, 200) + c.Check(root.Call("inc", 42.5), Equals, float64(43.5)) + root.Destroy() + }, + }, + { + Summary: "Create window with window root object", + QML: ` + import QtQuick.Window 2.0 + Window { title: "<title>"; width: 300; height: 200 } + `, + Done: func(c *TestData) { + win := c.component.CreateWindow(nil) + root := win.Root() + c.Check(root.String("title"), Equals, "<title>") + c.Check(root.Int("width"), Equals, 300) + c.Check(root.Int("height"), Equals, 200) + }, + }, + { + Summary: "Window is object", + QML: `Item {}`, + Done: func(c *TestData) { + win := c.component.CreateWindow(nil) + c.Assert(win.Int("status"), Equals, 1) // Ready + }, + }, + { + Summary: "Pass a *Value back into a method", + QML: `Rectangle { width: 300; function log(r) { console.log("Width is", r.width) } }`, + Done: func(c *TestData) { c.root.Call("log", c.root) }, + DoneLog: "Width is 300", + }, + { + Summary: "Create a QML-defined component in Go", + QML: `Item { property var comp: Component { Rectangle { width: 300 } } }`, + Done: func(c *TestData) { + rect := c.root.Object("comp").Create(nil) + c.Check(rect.Int("width"), Equals, 300) + c.Check(func() { c.root.Create(nil) }, Panics, "object is not a component") + c.Check(func() { c.root.CreateWindow(nil) }, Panics, "object is not a component") + }, + }, + { + Summary: "Call a Qt method that has no result", + QML: `Item { Component.onDestruction: console.log("item destroyed") }`, + Done: func(c *TestData) { + // Create a local instance to avoid double-destroying it. + root := c.component.Create(nil) + root.Call("deleteLater") + time.Sleep(100 * time.Millisecond) + }, + DoneLog: "item destroyed", + }, + { + Summary: "Errors connecting to QML signals", + QML: `Item { signal doIt() }`, + Done: func(c *TestData) { + c.Check(func() { c.root.On("missing", func() {}) }, Panics, `object does not expose a "missing" signal`) + c.Check(func() { c.root.On("doIt", func(s string) {}) }, Panics, `signal "doIt" has too few parameters for provided function`) + }, + }, + { + Summary: "Connect to a QML signal without parameters", + QML: ` + Item { + id: item + signal doIt() + function emitDoIt() { item.doIt() } + } + `, + Done: func(c *TestData) { + itWorks := false + c.root.On("doIt", func() { itWorks = true }) + c.Check(itWorks, Equals, false) + c.root.Call("emitDoIt") + c.Check(itWorks, Equals, true) + }, + }, + { + Summary: "Connect to a QML signal with a parameters", + QML: ` + Item { + id: item + signal doIt(string s, int n) + function emitDoIt() { item.doIt("<arg>", 123) } + } + `, + Done: func(c *TestData) { + var stack []interface{} + c.root.On("doIt", func() { stack = append(stack, "A") }) + c.root.On("doIt", func(s string) { stack = append(stack, "B", s) }) + c.root.On("doIt", func(s string, i int) { stack = append(stack, "C", s, i) }) + c.Check(stack, IsNil) + c.root.Call("emitDoIt") + c.Check(stack, DeepEquals, []interface{}{"A", "B", "<arg>", "C", "<arg>", 123}) + }, + }, + { + Summary: "Connect to a QML signal with an object parameter", + QML: `import QtWebKit 3.0; WebView{}`, + Done: func(c *TestData) { + url := "http://localhost:54321/" + done := make(chan bool) + c.root.On("navigationRequested", func(request qml.Object) { + c.Check(request.String("url"), Equals, url) + done <- true + }) + c.root.Set("url", url) + <-done + }, + }, + { + Summary: "Load image from Go provider", + Init: func(c *TestData) { + c.engine.AddImageProvider("myprov", func(id string, width, height int) image.Image { + return image.NewRGBA(image.Rect(0, 0, 200, 100)) + }) + }, + QML: ` + Image { + source: "image://myprov/myid.png" + Component.onCompleted: console.log("Size:", width, height) + } + `, + QMLLog: "Size: 200 100", + }, + { + Summary: "TypeName", + QML: `Item{}`, + Done: func(c *TestData) { c.Assert(c.root.TypeName(), Equals, "QQuickItem") }, + }, + { + Summary: "Custom Go type with painting", + QML: ` + Rectangle { + width: 200; height: 200 + color: "black" + GoRect { + width: 100; height: 100; x: 50; y: 50 + } + } + `, + Done: func(c *TestData) { + c.Assert(c.createdRect, HasLen, 0) + + window := c.component.CreateWindow(nil) + defer window.Destroy() + window.Show() + + // Qt doesn't hide the Window if we call it too quickly. :-( + time.Sleep(100 * time.Millisecond) + + c.Assert(c.createdRect, HasLen, 1) + c.Assert(c.createdRect[0].PaintCount, Equals, 1) + + image := window.Snapshot() + c.Assert(image.At(25, 25), Equals, color.RGBA{0, 0, 0, 255}) + c.Assert(image.At(100, 100), Equals, color.RGBA{255, 0, 0, 255}) + }, + }, + { + Summary: "Set a property with the wrong type", + QML: ` + import QtQuick.Window 2.0 + Window { Rectangle { objectName: "rect" } } + `, + Done: func(c *TestData) { + window := c.component.CreateWindow(nil) + defer window.Destroy() + + root := window.Root() // It's the window itself in this case + rect := root.ObjectByName("rect") + + c.Assert(func() { rect.Set("parent", root) }, Panics, + `cannot set property "parent" with type QQuickItem* to value of QQuickWindow*`) + c.Assert(func() { rect.Set("parent", 42) }, Panics, + `cannot set property "parent" with type QQuickItem* to value of int`) + c.Assert(func() { rect.Set("non_existent", 0) }, Panics, + `cannot set non-existent property "non_existent" on type QQuickRectangle`) + }, + }, + { + Summary: "Register a type converter for a signal parameter", + QML: ` + Item { + id: item + property Item self + signal testSignal(Item obj) + function emitSignal() { item.testSignal(item) } + function getSelf() { return item } + Component.onCompleted: { self = item } + } + `, + Done: func(c *TestData) { + type Wrapper struct{ Item qml.Object } + qml.RegisterConverter(c.root.TypeName(), func(engine *qml.Engine, item qml.Object) interface{} { + return &Wrapper{item} + }) + defer qml.RegisterConverter(c.root.TypeName(), nil) + + // Check that it works on signal parameters... + c.root.On("testSignal", func(wrapped *Wrapper) { + c.Check(wrapped.Item.Addr(), Equals, c.root.Addr()) + c.Logf("Signal has run.") + }) + c.root.Call("emitSignal") + + // ... on properties ... + wrapped, ok := c.root.Property("self").(*Wrapper) + if c.Check(ok, Equals, true) { + c.Check(wrapped.Item.Addr(), Equals, c.root.Addr()) + } + + // ... and on results. + wrapped, ok = c.root.Call("getSelf").(*Wrapper) + if c.Check(ok, Equals, true) { + c.Check(wrapped.Item.Addr(), Equals, c.root.Addr()) + } + + // Now unregister and ensure it got disabled. + qml.RegisterConverter(c.root.TypeName(), nil) + _, ok = c.root.Property("self").(*qml.Common) + c.Check(ok, Equals, true) + }, + DoneLog: "Signal has run.", + }, + { + Summary: "References handed out must not be GCd (issue #68)", + Init: func(c *TestData) { + type B struct{ S string } + type A struct{ B *B } + c.context.SetVar("a", &A{&B{}}) + }, + QML: `Item { function f() { var x = [[],[],[]]; gc(); if (!a.b) console.log("BUG"); } }`, + Done: func(c *TestData) { + for i := 0; i < 100; i++ { + c.root.Call("f") + } + }, + DoneLog: "!BUG", + }, +} + +var tablef = flag.String("tablef", "", "if provided, TestTable only runs tests with a summary matching the regexp") + +func (s *S) TestTable(c *C) { + var testData TestData + + types := []qml.TypeSpec{{ + Init: func(v *GoType, obj qml.Object) { + v.object = obj + v.StringValue = "<initial>" + testData.createdValue = append(testData.createdValue, v) + }, + }, { + Name: "NamedGoType", + Init: func(v *GoType, obj qml.Object) { + v.object = obj + v.StringValue = "<initial>" + testData.createdValue = append(testData.createdValue, v) + }, + }, { + Name: "GoSingleton", + Init: func(v *GoType, obj qml.Object) { + v.object = obj + v.StringValue = "<initial>" + testData.createdSingleton = append(testData.createdSingleton, v) + }, + Singleton: true, + }, { + Init: func(v *GoRect, obj qml.Object) { + testData.createdRect = append(testData.createdRect, v) + }, + }} + + qml.RegisterTypes("GoTypes", 4, 2, types) + + filter := regexp.MustCompile("") + if tablef != nil { + filter = regexp.MustCompile(*tablef) + } + + for i := range tests { + s.TearDownTest(c) + t := &tests[i] + header := fmt.Sprintf("----- Running table test %d: %s -----", i, t.Summary) + if !filter.MatchString(header) { + continue + } + c.Log(header) + s.SetUpTest(c) + + value := t.Value + s.context.SetVar("value", &value) + + testData = TestData{ + C: c, + value: &value, + engine: s.engine, + context: s.context, + } + + if t.Init != nil { + t.Init(&testData) + if c.Failed() { + c.FailNow() + } + } + + component, err := s.engine.LoadString("file.qml", "import QtQuick 2.0\nimport GoTypes 4.2\n"+strings.TrimSpace(t.QML)) + c.Assert(err, IsNil) + + logMark := c.GetTestLog() + + // The component instance is destroyed before the loop ends below, + // but do a defer to ensure it will be destroyed if the test fails. + root := component.Create(nil) + defer root.Destroy() + + testData.component = component + testData.root = root + + if t.QMLLog != "" { + logged := c.GetTestLog()[len(logMark):] + if t.QMLLog[0] == '!' { + c.Check(logged, Not(Matches), "(?s).*"+t.QMLLog[1:]+".*") + } else { + c.Check(logged, Matches, "(?s).*"+t.QMLLog+".*") + } + } + + if !reflect.DeepEqual(t.QMLValue, GoType{}) { + c.Check(value.StringValue, Equals, t.QMLValue.StringValue) + c.Check(value.StringAddrValue, Equals, t.QMLValue.StringAddrValue) + c.Check(value.BoolValue, Equals, t.QMLValue.BoolValue) + c.Check(value.IntValue, Equals, t.QMLValue.IntValue) + c.Check(value.Int64Value, Equals, t.QMLValue.Int64Value) + c.Check(value.Int32Value, Equals, t.QMLValue.Int32Value) + c.Check(value.Float64Value, Equals, t.QMLValue.Float64Value) + c.Check(value.Float32Value, Equals, t.QMLValue.Float32Value) + c.Check(value.AnyValue, Equals, t.QMLValue.AnyValue) + c.Check(value.IntsValue, DeepEquals, t.QMLValue.IntsValue) + c.Check(value.MapValue, DeepEquals, t.QMLValue.MapValue) + } + + if !c.Failed() { + logMark := c.GetTestLog() + + if t.Done != nil { + t.Done(&testData) + } + + if t.DoneLog != "" { + logged := c.GetTestLog()[len(logMark):] + if t.DoneLog[0] == '!' { + c.Check(logged, Not(Matches), "(?s).*"+t.DoneLog[1:]+".*") + } else { + c.Check(logged, Matches, "(?s).*"+t.DoneLog+".*") + } + } + + if !reflect.DeepEqual(t.DoneValue, GoType{}) { + c.Check(value.StringValue, Equals, t.DoneValue.StringValue) + c.Check(value.StringAddrValue, Equals, t.DoneValue.StringAddrValue) + c.Check(value.BoolValue, Equals, t.DoneValue.BoolValue) + c.Check(value.IntValue, Equals, t.DoneValue.IntValue) + c.Check(value.Int64Value, Equals, t.DoneValue.Int64Value) + c.Check(value.Int32Value, Equals, t.DoneValue.Int32Value) + c.Check(value.Float64Value, Equals, t.DoneValue.Float64Value) + c.Check(value.Float32Value, Equals, t.DoneValue.Float32Value) + c.Check(value.AnyValue, Equals, t.DoneValue.AnyValue) + c.Check(value.IntsValue, DeepEquals, t.DoneValue.IntsValue) + c.Check(value.MapValue, DeepEquals, t.DoneValue.MapValue) + } + } + + root.Destroy() + + if c.Failed() { + c.FailNow() // So relevant logs are at the bottom. + } + } +} |