1package commands
2
3import (
4	"fmt"
5	"reflect"
6
7	"github.com/leanovate/gopter"
8	"github.com/leanovate/gopter/gen"
9)
10
11type shrinkableCommand struct {
12	command  Command
13	shrinker gopter.Shrinker
14}
15
16func (s shrinkableCommand) shrink() gopter.Shrink {
17	return s.shrinker(s.command).Map(func(command Command) shrinkableCommand {
18		return shrinkableCommand{
19			command:  command,
20			shrinker: s.shrinker,
21		}
22	})
23}
24
25func (s shrinkableCommand) String() string {
26	return fmt.Sprintf("%v", s.command)
27}
28
29type actions struct {
30	// initialStateProvider has to reset/recreate the initial state exactly the
31	// same every time.
32	initialStateProvider func() State
33	sequentialCommands   []shrinkableCommand
34	// parallel commands will come later
35}
36
37func (a *actions) String() string {
38	return fmt.Sprintf("initialState=%v sequential=%s", a.initialStateProvider(), a.sequentialCommands)
39}
40
41func (a *actions) run(systemUnderTest SystemUnderTest) (*gopter.PropResult, error) {
42	state := a.initialStateProvider()
43	propResult := &gopter.PropResult{Status: gopter.PropTrue}
44	for _, shrinkableCommand := range a.sequentialCommands {
45		if !shrinkableCommand.command.PreCondition(state) {
46			return &gopter.PropResult{Status: gopter.PropFalse}, nil
47		}
48		result := shrinkableCommand.command.Run(systemUnderTest)
49		state = shrinkableCommand.command.NextState(state)
50		propResult = propResult.And(shrinkableCommand.command.PostCondition(state, result))
51	}
52	return propResult, nil
53}
54
55type sizedCommands struct {
56	state    State
57	commands []shrinkableCommand
58}
59
60func actionsShrinker(v interface{}) gopter.Shrink {
61	a := v.(*actions)
62	elementShrinker := gopter.Shrinker(func(v interface{}) gopter.Shrink {
63		return v.(shrinkableCommand).shrink()
64	})
65	return gen.SliceShrinker(elementShrinker)(a.sequentialCommands).Map(func(v []shrinkableCommand) *actions {
66		return &actions{
67			initialStateProvider: a.initialStateProvider,
68			sequentialCommands:   v,
69		}
70	})
71}
72
73func genActions(commands Commands) gopter.Gen {
74	genInitialState := commands.GenInitialState()
75	genInitialStateProvider := gopter.Gen(func(params *gopter.GenParameters) *gopter.GenResult {
76		seed := params.NextInt64()
77		return gopter.NewGenResult(func() State {
78			paramsWithSeed := params.CloneWithSeed(seed)
79			if initialState, ok := genInitialState(paramsWithSeed).Retrieve(); ok {
80				return initialState
81			}
82			return nil
83		}, gopter.NoShrinker)
84	}).SuchThat(func(initialStateProvoder func() State) bool {
85		state := initialStateProvoder()
86		return state != nil && commands.InitialPreCondition(state)
87	})
88	return genInitialStateProvider.FlatMap(func(v interface{}) gopter.Gen {
89		initialStateProvider := v.(func() State)
90		return genSizedCommands(commands, initialStateProvider).Map(func(v sizedCommands) *actions {
91			return &actions{
92				initialStateProvider: initialStateProvider,
93				sequentialCommands:   v.commands,
94			}
95		}).SuchThat(func(actions *actions) bool {
96			state := actions.initialStateProvider()
97			for _, shrinkableCommand := range actions.sequentialCommands {
98				if !shrinkableCommand.command.PreCondition(state) {
99					return false
100				}
101				state = shrinkableCommand.command.NextState(state)
102			}
103			return true
104		}).WithShrinker(actionsShrinker)
105	}, reflect.TypeOf((*actions)(nil)))
106}
107
108func genSizedCommands(commands Commands, initialStateProvider func() State) gopter.Gen {
109	return func(genParams *gopter.GenParameters) *gopter.GenResult {
110		sizedCommandsGen := gen.Const(sizedCommands{
111			state:    initialStateProvider(),
112			commands: make([]shrinkableCommand, 0, genParams.MaxSize),
113		})
114		for i := 0; i < genParams.MaxSize; i++ {
115			sizedCommandsGen = sizedCommandsGen.FlatMap(func(v interface{}) gopter.Gen {
116				prev := v.(sizedCommands)
117				return gen.RetryUntil(commands.GenCommand(prev.state), func(command Command) bool {
118					return command.PreCondition(prev.state)
119				}, 100).MapResult(func(result *gopter.GenResult) *gopter.GenResult {
120					value, ok := result.Retrieve()
121					if !ok {
122						return gopter.NewEmptyResult(reflect.TypeOf(sizedCommands{}))
123					}
124					command := value.(Command)
125					return gopter.NewGenResult(
126						sizedCommands{
127							state: command.NextState(prev.state),
128							commands: append(prev.commands, shrinkableCommand{
129								command:  command,
130								shrinker: result.Shrinker,
131							}),
132						},
133						gopter.NoShrinker,
134					)
135				})
136			}, reflect.TypeOf(sizedCommands{}))
137		}
138		return sizedCommandsGen(genParams)
139	}
140}
141