diff --git a/docs/settingup.md b/docs/settingup.md index 8f16259b..51021bb1 100644 --- a/docs/settingup.md +++ b/docs/settingup.md @@ -2261,6 +2261,30 @@ Unsubscribe from all currently subscribed objects. ```
+
+stepdimension + +## StepDimension action + +Cycle a step in a cyclic dimension + +* `id`: library ID of the cyclic dimension + +### Example + +Cycle one step in the dimension with library ID `aBc123`. + +```json +{ + "action": "stepdimension", + "settings":{ + "id": "aBc123" + } +} +``` + +
+
diff --git a/generatedocs/data/actions/stepdimension/description.md b/generatedocs/data/actions/stepdimension/description.md new file mode 100644 index 00000000..650abcc0 --- /dev/null +++ b/generatedocs/data/actions/stepdimension/description.md @@ -0,0 +1,3 @@ +## StepDimension action + +Cycle a step in a cyclic dimension diff --git a/generatedocs/data/actions/stepdimension/examples.md b/generatedocs/data/actions/stepdimension/examples.md new file mode 100644 index 00000000..f86cf9a0 --- /dev/null +++ b/generatedocs/data/actions/stepdimension/examples.md @@ -0,0 +1,12 @@ +### Example + +Cycle one step in the dimension with library ID `aBc123`. + +```json +{ + "action": "stepdimension", + "settings":{ + "id": "aBc123" + } +} +``` diff --git a/generatedocs/data/groups/groups.json b/generatedocs/data/groups/groups.json index 190f2efe..bf6e9117 100644 --- a/generatedocs/data/groups/groups.json +++ b/generatedocs/data/groups/groups.json @@ -38,7 +38,8 @@ "thinktime", "unpublishbookmark", "unpublishsheet", - "unsubscribeobjects" + "unsubscribeobjects", + "stepdimension" ] }, { diff --git a/generatedocs/data/params.json b/generatedocs/data/params.json index 3cfda0f8..8cb92c98 100644 --- a/generatedocs/data/params.json +++ b/generatedocs/data/params.json @@ -558,6 +558,9 @@ "setscriptvar.value": [ "Value to set to variable (supports the use of [session variables](#session_variables))." ], + "stepdimension.id": [ + "library ID of the cyclic dimension" + ], "subscribeobjects.clear" : [ "Remove any previously subscribed objects from the subscription list." ], diff --git a/generatedocs/generated/documentation.go b/generatedocs/generated/documentation.go index 0a88f56a..71326c1e 100644 --- a/generatedocs/generated/documentation.go +++ b/generatedocs/generated/documentation.go @@ -150,6 +150,10 @@ var ( Description: "## SmartSearch action\n\nPerform a Smart Search in Sense app to find suggested selections.\n", Examples: "\n### Examples\n\n#### Search with one search term\n```json\n{\n \"action\": \"smartsearch\",\n \"label\": \"one term search\",\n \"settings\": {\n \"searchtextlist\": [\n \"term1\"\n ]\n }\n}\n```\n\n#### Search with two search terms\n```json\n{\n \"action\": \"smartsearch\",\n \"label\": \"two term search\",\n \"settings\": {\n \"searchtextlist\": [\n \"term1 term2\"\n ]\n }\n}\n```\n\n#### Search with random selection of search text from list\n```json\n{\n \"action\": \"smartsearch\",\n \"settings\": {\n \"searchtextlist\": [\n \"text1\",\n \"text2\",\n \"text3\"\n ]\n }\n}\n```\n\n#### Search with random selection of search text from file\n```json\n{\n \"action\": \"smartsearch\",\n \"settings\": {\n \"searchtextsource\": \"searchtextfile\",\n \"searchtextfile\": \"data/searchtexts.txt\"\n }\n}\n```\n##### `data/searchtexts.txt`\n```\nsearch text\n\"quoted search text\"\nanother search text\n```\n\n#### Simulate pasting search text\n\nThe default behavior is to simulate typing at normal speed.\n```json\n{\n \"action\": \"smartsearch\",\n \"settings\": {\n \"pastesearchtext\": true,\n \"searchtextlist\": [\n \"text1\"\n ]\n }\n}\n```\n\n#### Make a random selection from search results\n```json\n{\n \"action\": \"smartsearch\",\n \"settings\": {\n \"searchtextlist\": [\n \"term1\"\n ],\n \"makeselection\": true,\n \"selectionthinktime\": {\n \"type\": \"static\",\n \"delay\": 2\n }\n }\n}\n```\n\n#### Search with one search term including spaces\n```json\n{\n \"action\": \"smartsearch\",\n \"settings\": {\n \"searchtextlist\": [\n \"\\\"word1 word2\\\"\"\n ]\n }\n}\n```\n\n#### Search with two search terms, one of them including spaces\n```json\n{\n \"action\": \"smartsearch\",\n \"label\": \"two term search, one including spaces\",\n \"settings\": {\n \"searchtextlist\": [\n \"\\\"word1 word2\\\" term2\"\n ]\n }\n}\n```\n\n#### Search with one search term including double quote\n```json\n{\n \"action\": \"smartsearch\",\n \"label\": \"one term search including spaces\",\n \"settings\": {\n \"searchtext\":\n \"searchtextlist\": [\n \"\\\\\\\"hello\"\n ]\n }\n}\n```\n", }, + "stepdimension": { + Description: "## StepDimension action\n\nCycle a step in a cyclic dimension\n", + Examples: "### Example\n\nCycle one step in the dimension with library ID `aBc123`.\n\n```json\n{\n \"action\": \"stepdimension\",\n \"settings\":{\n \"id\": \"aBc123\"\n }\n}\n```\n", + }, "subscribeobjects": { Description: "## Subscribeobjects action\n\nSubscribe to any object in the currently active app.\n", Examples: "### Example\n\nSubscribe to two objects in the currently active app and remove any previous subscriptions. \n\n```json\n{\n \"action\" : \"subscribeobjects\",\n \"label\" : \"clear subscriptions and subscribe to mBshXB and f2a50cb3-a7e1-40ac-a015-bc4378773312\",\n \"disabled\": false,\n \"settings\" : {\n \"clear\" : true,\n \"ids\" : [\"mBshXB\", \"f2a50cb3-a7e1-40ac-a015-bc4378773312\"]\n }\n}\n```\n\nSubscribe to an additional single object (or a list of objects) in the currently active app, adding the new subscription to any previous subscriptions.\n\n```json\n{\n \"action\" : \"subscribeobjects\",\n \"label\" : \"add c430d8e2-0f05-49f1-aa6f-7234e325dc35 to currently subscribed objects\",\n \"disabled\": false,\n \"settings\" : {\n \"clear\" : false,\n \"ids\" : [\"c430d8e2-0f05-49f1-aa6f-7234e325dc35\"]\n }\n}\n```", @@ -349,6 +353,7 @@ var ( "smartsearch.searchtextlist": {"List of of strings used for searching."}, "smartsearch.searchtextsource": {"Source for list of strings used for searching.", "`searchtextlist` (default)", "`searchtextfile`"}, "smartsearch.selectionthinktime": {"Think time before selection if `makeselection` is `true`, defaults to a 1 second delay."}, + "stepdimension.id": {"library ID of the cyclic dimension"}, "subscribeobjects.clear": {"Remove any previously subscribed objects from the subscription list."}, "subscribeobjects.ids": {"List of object IDs to subscribe to."}, "thinktime.delay": {"Delay (seconds), used with type `static`."}, @@ -399,7 +404,7 @@ var ( { Name: "commonActions", Title: "Common actions", - Actions: []string{"applybookmark", "askhubadvisor", "changesheet", "clearall", "clearfield", "clickactionbutton", "containertab", "createbookmark", "createsheet", "deletebookmark", "deletesheet", "disconnectapp", "disconnectenvironment", "dosave", "duplicatesheet", "getscript", "iterated", "listboxselect", "objectsearch", "openapp", "productversion", "publishbookmark", "publishsheet", "randomaction", "reload", "select", "setscript", "setscriptvar", "setsensevariable", "sheetchanger", "smartsearch", "subscribeobjects", "thinktime", "unpublishbookmark", "unpublishsheet", "unsubscribeobjects"}, + Actions: []string{"applybookmark", "askhubadvisor", "changesheet", "clearall", "clearfield", "clickactionbutton", "containertab", "createbookmark", "createsheet", "deletebookmark", "deletesheet", "disconnectapp", "disconnectenvironment", "dosave", "duplicatesheet", "getscript", "iterated", "listboxselect", "objectsearch", "openapp", "productversion", "publishbookmark", "publishsheet", "randomaction", "reload", "select", "setscript", "setscriptvar", "setsensevariable", "sheetchanger", "smartsearch", "subscribeobjects", "thinktime", "unpublishbookmark", "unpublishsheet", "unsubscribeobjects", "stepdimension"}, DocEntry: common.DocEntry{ Description: "# Common actions\n\nThese actions are applicable for most types of Qlik Sense deployments.\n\n**Note:** It is recommended to prepend the actions listed here with an `openapp` action as most of them perform operations in an app context (such as making selections or changing sheets).\n", Examples: "", diff --git a/globals/constant/sense.go b/globals/constant/sense.go index ed4e8e21..7c8ec190 100644 --- a/globals/constant/sense.go +++ b/globals/constant/sense.go @@ -38,3 +38,10 @@ const ( DataReductionModeClustered = "C" DataReductionModeStacked = "ST" ) + +// NxDimensionInfoGrouping +const ( + NxDimensionInfoGroupingNone = "N" + NxDimensionInfoGroupingHiearchy = "H" + NxDimensionInfoGroupingCollection = "C" +) diff --git a/scenario/actionhandler.go b/scenario/actionhandler.go index bc64a9ee..c20428c8 100644 --- a/scenario/actionhandler.go +++ b/scenario/actionhandler.go @@ -148,6 +148,7 @@ const ( ActionObjectSearch = "objectsearch" ActionGetScript = "getscript" ActionChangeSteam = "changestream" + ActionStepDimension = "stepdimension" ) // Scenario actions needs an entry in actionHandler @@ -260,6 +261,7 @@ func ResetDefaultActions() { ActionObjectSearch: ObjectSearchSettings{}, ActionGetScript: GetscriptSettings{}, ActionChangeSteam: ChangestreamSettings{}, + ActionStepDimension: StepDimensionSettings{}, } } diff --git a/scenario/stepdimension.go b/scenario/stepdimension.go new file mode 100644 index 00000000..c9ccec6c --- /dev/null +++ b/scenario/stepdimension.go @@ -0,0 +1,54 @@ +package scenario + +import ( + "context" + + "github.com/pkg/errors" + "github.com/qlik-oss/gopherciser/action" + "github.com/qlik-oss/gopherciser/connection" + "github.com/qlik-oss/gopherciser/session" +) + +type ( + // StepDimensionSettings cycle a step in a cyclic dimension + StepDimensionSettings struct { + Id string `json:"id" doc-key:"stepdimension.id"` + } +) + +// Validate StepDimensionSettings action (Implements ActionSettings interface) +func (settings StepDimensionSettings) Validate() ([]string, error) { + if settings.Id == "" { + return nil, errors.Errorf("Id not set for %s", ActionStepDimension) + } + return nil, nil +} + +// Execute StepDimensionSettings action (Implements ActionSettings interface) +func (settings StepDimensionSettings) Execute(sessionState *session.State, actionState *action.State, connection *connection.ConnectionSettings, label string, reset func()) { + if sessionState.Connection == nil || sessionState.Connection.Sense() == nil { + actionState.AddErrors(errors.New("Not connected to a Sense environment")) + return + } + + app := sessionState.Connection.Sense().CurrentApp + if app == nil { + actionState.AddErrors(errors.New("Not connected to a Sense app")) + return + } + + dim, err := app.GetDimension(sessionState, actionState, settings.Id) + if err != nil { + actionState.AddErrors(err) + return + } + + err = sessionState.SendRequest(actionState, func(ctx context.Context) error { + return dim.StepCycle(ctx, 1) + }) + if err != nil { + actionState.AddErrors(err) + } + + sessionState.Wait(actionState) // Await all async requests, e.g. those triggered on changed objects +} diff --git a/scripts/lint.sh b/scripts/lint.sh index 2c02e000..60e54087 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,24 +3,6 @@ # Purpose: This script lints the code. # Instructions: make lint -# On darwin you can also install with homebrew -# brew install golangci/tap/golangci-lint - -# set OS -OS= -if [[ "$OSTYPE" == "linux-gnu" ]]; then - OS=Linux -elif [[ "$OSTYPE" == "darwin"* ]]; then - # Mac OSX - OS=Darwin -elif [[ "$OSTYPE" == "cygwin" ]]; then - # POSIX compatibility layer and Linux environment emulation for Windows - OS=Windows -else - # Unknown, assume Linux - OS=Linux -fi - # set lint level LINTLEVEL=${1:-DEFAULT} @@ -48,15 +30,12 @@ fi echo Running lint case $LINTLEVEL in # minimal amount of linting currently running clean. More linters and and rules will be added as more lint errors are fixed. -# Current status: -# linters currently looked at: govet -# subrules currently disabled: -# * structtag : We override tags in engima, so structtag complains about repeating tags, found no way to tell it this is intended. +# currently no subrules disabled in "min" MIN) - "$GOPATH"/bin/golangci-lint run -D structcheck --timeout 5m + "$GOPATH"/bin/golangci-lint run --timeout 5m ;; -# Default set of linters +# "full" set of linters *) - "$GOPATH"/bin/golangci-lint run + "$GOPATH"/bin/golangci-lint run --timeout 5m ;; esac diff --git a/senseobjects/app.go b/senseobjects/app.go index fe32f3ca..1a386367 100644 --- a/senseobjects/app.go +++ b/senseobjects/app.go @@ -22,6 +22,7 @@ type ( loadmodellist *LoadModelList fieldlist *FieldList dimensionList *DimensionList + dimensions map[string]*enigma.GenericDimension appPropsList *AppPropsList } @@ -542,3 +543,35 @@ func (app *App) setDimensionList(SessionState SessionState, dl *DimensionList) { } app.dimensionList = dl } + +func (app *App) GetDimension(sessionState SessionState, actionState *action.State, id string) (*enigma.GenericDimension, error) { + dim := app.dimensions[id] + if dim != nil { + return dim, nil + } + + err := sessionState.SendRequest(actionState, func(ctx context.Context) error { + var err error + dim, err = app.Doc.GetDimension(ctx, id) + return err + }) + if err != nil { + return nil, err + } + + app.addDimension(dim) + + return dim, nil +} + +func (app *App) addDimension(dim *enigma.GenericDimension) { + app.mutex.Lock() + defer app.mutex.Unlock() + + if app.dimensions == nil { + app.dimensions = map[string]*enigma.GenericDimension{dim.GenericId: dim} + return + } + + app.dimensions[dim.GenericId] = dim +} diff --git a/session/objecthandling.go b/session/objecthandling.go index f315e25c..9443662d 100644 --- a/session/objecthandling.go +++ b/session/objecthandling.go @@ -344,7 +344,7 @@ func SetListObject(rawLayout json.RawMessage, obj *enigmahandlers.Object, path h return nil } -func SetHyperCube(rawLayout json.RawMessage, obj *enigmahandlers.Object, path helpers.DataPath) error { +func SetHyperCube(sessionState *State, actionState *action.State, rawLayout json.RawMessage, obj *enigmahandlers.Object, path helpers.DataPath) error { rawHyperCube, err := path.Lookup(rawLayout) if err != nil { return errors.Wrap(err, "error getting hypercube") @@ -356,6 +356,27 @@ func SetHyperCube(rawLayout json.RawMessage, obj *enigmahandlers.Object, path he } obj.SetHyperCube(hyperCube) + + // Look for cyclic dimensions and add to app sessionobjects + if len(hyperCube.DimensionInfo) > 0 { + for i, dim := range hyperCube.DimensionInfo { + if dim != nil && dim.Grouping == constant.NxDimensionInfoGroupingCollection { + app, err := sessionState.CurrentSenseApp() + if err != nil { + return errors.WithStack(err) + } + if dim.LibraryId == "" { + sessionState.LogEntry.Logf(logger.WarningLevel, "object<%s> dim<%d> has grouping, but no library ID", obj.ID, i) + continue + } + // GetDimension (adds it to sessionobjects list) + if _, err = app.GetDimension(sessionState, actionState, dim.LibraryId); err != nil { + actionState.AddErrors(err) + } + } + } + } + return nil } @@ -888,7 +909,7 @@ func SetObjectData(sessionState *State, actionState *action.State, rawLayout jso return errors.Errorf( "object<%s> is defined as hypercube carrier, but has not hypercube path definition", enigmaObject.GenericType) } - if err := SetHyperCube(rawLayout, obj, objectDef.DataDef.Path); err != nil { + if err := SetHyperCube(sessionState, actionState, rawLayout, obj, objectDef.DataDef.Path); err != nil { return errors.Wrapf(err, "object<%s> type<%s>", obj.ID, enigmaObject.GenericType) } default: