1
0

parser_test.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. package workflow
  2. import (
  3. "testing"
  4. )
  5. func TestParseWorkflowDefinition(t *testing.T) {
  6. tests := []struct {
  7. name string
  8. jsonDef string
  9. wantErr bool
  10. }{
  11. {
  12. name: "valid simple workflow",
  13. jsonDef: `{
  14. "nodes": {
  15. "start": {
  16. "type": "task",
  17. "title": "Start",
  18. "content": "Start task",
  19. "dependsOn": []
  20. },
  21. "end": {
  22. "type": "task",
  23. "title": "End",
  24. "content": "End task",
  25. "dependsOn": ["start"]
  26. }
  27. }
  28. }`,
  29. wantErr: false,
  30. },
  31. {
  32. name: "valid parallel workflow",
  33. jsonDef: `{
  34. "nodes": {
  35. "start": {
  36. "type": "task",
  37. "title": "Start",
  38. "content": "Start task",
  39. "dependsOn": []
  40. },
  41. "parallel1": {
  42. "type": "task",
  43. "title": "Parallel 1",
  44. "content": "Parallel task 1",
  45. "dependsOn": ["start"]
  46. },
  47. "parallel2": {
  48. "type": "task",
  49. "title": "Parallel 2",
  50. "content": "Parallel task 2",
  51. "dependsOn": ["start"]
  52. },
  53. "join": {
  54. "type": "join",
  55. "title": "Join",
  56. "content": "Join parallel branches",
  57. "dependsOn": ["parallel1", "parallel2"]
  58. }
  59. }
  60. }`,
  61. wantErr: false,
  62. },
  63. {
  64. name: "invalid JSON",
  65. jsonDef: `{invalid}`,
  66. wantErr: true,
  67. },
  68. }
  69. for _, tt := range tests {
  70. t.Run(tt.name, func(t *testing.T) {
  71. def, err := ParseWorkflowDefinition(tt.jsonDef)
  72. if (err != nil) != tt.wantErr {
  73. t.Errorf("ParseWorkflowDefinition() error = %v, wantErr %v", err, tt.wantErr)
  74. return
  75. }
  76. if !tt.wantErr && def == nil {
  77. t.Error("ParseWorkflowDefinition() returned nil definition")
  78. }
  79. })
  80. }
  81. }
  82. func TestParseAndValidate(t *testing.T) {
  83. tests := []struct {
  84. name string
  85. jsonDef string
  86. wantErr bool
  87. }{
  88. {
  89. name: "empty definition",
  90. jsonDef: `{}`,
  91. wantErr: true,
  92. },
  93. {
  94. name: "node missing type",
  95. jsonDef: `{
  96. "nodes": {
  97. "start": {
  98. "title": "Start"
  99. }
  100. }
  101. }`,
  102. wantErr: true,
  103. },
  104. {
  105. name: "invalid node type",
  106. jsonDef: `{
  107. "nodes": {
  108. "start": {
  109. "type": "invalid",
  110. "title": "Start"
  111. }
  112. }
  113. }`,
  114. wantErr: true,
  115. },
  116. {
  117. name: "dependency on non-existent node",
  118. jsonDef: `{
  119. "nodes": {
  120. "start": {
  121. "type": "task",
  122. "title": "Start",
  123. "dependsOn": ["nonexistent"]
  124. }
  125. }
  126. }`,
  127. wantErr: true,
  128. },
  129. }
  130. for _, tt := range tests {
  131. t.Run(tt.name, func(t *testing.T) {
  132. def, err := ParseWorkflowDefinition(tt.jsonDef)
  133. if err != nil {
  134. if !tt.wantErr {
  135. t.Errorf("ParseWorkflowDefinition() unexpected error = %v", err)
  136. }
  137. return
  138. }
  139. // Parse succeeded, now validate
  140. err = def.Validate()
  141. if (err != nil) != tt.wantErr {
  142. t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
  143. }
  144. })
  145. }
  146. }
  147. func TestValidate(t *testing.T) {
  148. tests := []struct {
  149. name string
  150. jsonDef string
  151. wantErr bool
  152. }{
  153. {
  154. name: "valid workflow",
  155. jsonDef: `{
  156. "nodes": {
  157. "start": {"type": "task", "title": "Start", "dependsOn": []},
  158. "end": {"type": "task", "title": "End", "dependsOn": ["start"]}
  159. }
  160. }`,
  161. wantErr: false,
  162. },
  163. {
  164. name: "cycle detection",
  165. jsonDef: `{
  166. "nodes": {
  167. "a": {"type": "task", "title": "A", "dependsOn": ["b"]},
  168. "b": {"type": "task", "title": "B", "dependsOn": ["a"]}
  169. }
  170. }`,
  171. wantErr: true,
  172. },
  173. {
  174. name: "self-referential cycle",
  175. jsonDef: `{
  176. "nodes": {
  177. "a": {"type": "task", "title": "A", "dependsOn": ["a"]}
  178. }
  179. }`,
  180. wantErr: true,
  181. },
  182. {
  183. name: "complex cycle",
  184. jsonDef: `{
  185. "nodes": {
  186. "a": {"type": "task", "title": "A", "dependsOn": []},
  187. "b": {"type": "task", "title": "B", "dependsOn": ["a"]},
  188. "c": {"type": "task", "title": "C", "dependsOn": ["b"]},
  189. "d": {"type": "task", "title": "D", "dependsOn": ["c"]},
  190. "e": {"type": "task", "title": "E", "dependsOn": ["d", "b"]}
  191. }
  192. }`,
  193. wantErr: false,
  194. },
  195. }
  196. for _, tt := range tests {
  197. t.Run(tt.name, func(t *testing.T) {
  198. def, err := ParseWorkflowDefinition(tt.jsonDef)
  199. if err != nil {
  200. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  201. }
  202. err = def.Validate()
  203. if (err != nil) != tt.wantErr {
  204. t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
  205. }
  206. })
  207. }
  208. }
  209. func TestGetRootNodes(t *testing.T) {
  210. jsonDef := `{
  211. "nodes": {
  212. "start": {"type": "task", "title": "Start", "dependsOn": []},
  213. "middle": {"type": "task", "title": "Middle", "dependsOn": ["start"]},
  214. "end": {"type": "task", "title": "End", "dependsOn": ["middle"]}
  215. }
  216. }`
  217. def, err := ParseWorkflowDefinition(jsonDef)
  218. if err != nil {
  219. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  220. }
  221. roots := def.GetRootNodes()
  222. if len(roots) != 1 {
  223. t.Errorf("GetRootNodes() returned %d roots, want 1", len(roots))
  224. }
  225. if len(roots) > 0 && roots[0] != "start" {
  226. t.Errorf("GetRootNodes() returned %v, want [start]", roots)
  227. }
  228. }
  229. func TestGetDependentNodes(t *testing.T) {
  230. jsonDef := `{
  231. "nodes": {
  232. "start": {"type": "task", "title": "Start", "dependsOn": []},
  233. "branch1": {"type": "task", "title": "Branch 1", "dependsOn": ["start"]},
  234. "branch2": {"type": "task", "title": "Branch 2", "dependsOn": ["start"]},
  235. "end": {"type": "task", "title": "End", "dependsOn": ["branch1", "branch2"]}
  236. }
  237. }`
  238. def, err := ParseWorkflowDefinition(jsonDef)
  239. if err != nil {
  240. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  241. }
  242. dependents := def.GetDependentNodes("start")
  243. if len(dependents) != 2 {
  244. t.Errorf("GetDependentNodes(start) returned %d dependents, want 2", len(dependents))
  245. }
  246. dependents = def.GetDependentNodes("branch1")
  247. if len(dependents) != 1 {
  248. t.Errorf("GetDependentNodes(branch1) returned %d dependents, want 1", len(dependents))
  249. }
  250. }
  251. func TestGetReadyNodes(t *testing.T) {
  252. jsonDef := `{
  253. "nodes": {
  254. "start": {"type": "task", "title": "Start", "dependsOn": []},
  255. "middle": {"type": "task", "title": "Middle", "dependsOn": ["start"]},
  256. "end": {"type": "task", "title": "End", "dependsOn": ["middle"]}
  257. }
  258. }`
  259. def, err := ParseWorkflowDefinition(jsonDef)
  260. if err != nil {
  261. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  262. }
  263. // Initially, only start should be ready
  264. ready := def.GetReadyNodes(map[string]bool{})
  265. if len(ready) != 1 || ready[0] != "start" {
  266. t.Errorf("GetReadyNodes({}) = %v, want [start]", ready)
  267. }
  268. // After start is executed, middle should be ready
  269. ready = def.GetReadyNodes(map[string]bool{"start": true})
  270. if len(ready) != 1 || ready[0] != "middle" {
  271. t.Errorf("GetReadyNodes({start:true}) = %v, want [middle]", ready)
  272. }
  273. // After middle is executed, end should be ready
  274. ready = def.GetReadyNodes(map[string]bool{"start": true, "middle": true})
  275. if len(ready) != 1 || ready[0] != "end" {
  276. t.Errorf("GetReadyNodes({start:true, middle:true}) = %v, want [end]", ready)
  277. }
  278. }
  279. func TestParallelNodeTypes(t *testing.T) {
  280. jsonDef := `{
  281. "nodes": {
  282. "start": {"type": "task", "title": "Start", "dependsOn": []},
  283. "parallel": {"type": "parallel", "title": "Parallel", "dependsOn": ["start"]},
  284. "join": {"type": "join", "title": "Join", "dependsOn": ["parallel"]}
  285. }
  286. }`
  287. def, err := ParseWorkflowDefinition(jsonDef)
  288. if err != nil {
  289. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  290. }
  291. if err := def.Validate(); err != nil {
  292. t.Errorf("Validate() error = %v, want nil", err)
  293. }
  294. // Verify node types
  295. if def.Nodes["parallel"].Type != NodeTypeParallel {
  296. t.Errorf("parallel node type = %v, want %v", def.Nodes["parallel"].Type, NodeTypeParallel)
  297. }
  298. if def.Nodes["join"].Type != NodeTypeJoin {
  299. t.Errorf("join node type = %v, want %v", def.Nodes["join"].Type, NodeTypeJoin)
  300. }
  301. }
  302. func TestConditionNodeType(t *testing.T) {
  303. jsonDef := `{
  304. "nodes": {
  305. "start": {"type": "task", "title": "Start", "dependsOn": []},
  306. "condition": {"type": "condition", "title": "Condition", "dependsOn": ["start"]}
  307. }
  308. }`
  309. def, err := ParseWorkflowDefinition(jsonDef)
  310. if err != nil {
  311. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  312. }
  313. if err := def.Validate(); err != nil {
  314. t.Errorf("Validate() error = %v, want nil", err)
  315. }
  316. if def.Nodes["condition"].Type != NodeTypeCondition {
  317. t.Errorf("condition node type = %v, want %v", def.Nodes["condition"].Type, NodeTypeCondition)
  318. }
  319. }
  320. func TestTriggerNodeType(t *testing.T) {
  321. jsonDef := `{
  322. "nodes": {
  323. "trigger": {"type": "trigger", "title": "Trigger", "dependsOn": []},
  324. "task": {"type": "task", "title": "Task", "dependsOn": ["trigger"]}
  325. }
  326. }`
  327. def, err := ParseWorkflowDefinition(jsonDef)
  328. if err != nil {
  329. t.Fatalf("ParseWorkflowDefinition() error = %v", err)
  330. }
  331. if err := def.Validate(); err != nil {
  332. t.Errorf("Validate() error = %v, want nil", err)
  333. }
  334. if def.Nodes["trigger"].Type != NodeTypeTrigger {
  335. t.Errorf("trigger node type = %v, want %v", def.Nodes["trigger"].Type, NodeTypeTrigger)
  336. }
  337. }