feat: step mocking for flows

This commit is contained in:
Ruben Fiszel
2023-06-22 12:01:08 +02:00
parent 251de9b173
commit 734f75d493
14 changed files with 221 additions and 50 deletions
+16 -7
View File
@@ -1015,7 +1015,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
FlowModule {
id: "b".to_string(),
@@ -1044,7 +1045,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
}],
},
stop_after_if: Default::default(),
@@ -1052,7 +1054,8 @@ async fn test_deno_flow(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
],
same_worker: false,
@@ -1147,7 +1150,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
FlowModule {
id: "b".to_string(),
@@ -1187,7 +1191,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
FlowModule {
id: "e".to_string(),
@@ -1214,7 +1219,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None
},
],
},
@@ -1224,6 +1230,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
retry: None,
sleep: None,
cache_ttl: None,
mock: None,
},
FlowModule {
id: "c".to_string(),
@@ -1258,7 +1265,9 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
],
same_worker: true,
+8 -4
View File
@@ -652,7 +652,8 @@ mod tests {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
FlowModule {
id: "b".to_string(),
@@ -672,7 +673,8 @@ mod tests {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
FlowModule {
id: "c".to_string(),
@@ -690,7 +692,8 @@ mod tests {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
},
],
failure_module: Some(FlowModule {
@@ -708,7 +711,8 @@ mod tests {
suspend: Default::default(),
retry: None,
sleep: None,
cache_ttl: None
cache_ttl: None,
mock: None,
}),
same_worker: false,
};
+9
View File
@@ -156,6 +156,13 @@ pub struct Suspend {
pub resume_form: Option<serde_json::Value>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Mock {
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_value: Option<serde_json::Value>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FlowModule {
#[serde(default = "default_id")]
@@ -168,6 +175,8 @@ pub struct FlowModule {
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<Suspend>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mock: Option<Mock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<Retry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sleep: Option<InputTransform>,
+1
View File
@@ -589,6 +589,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
summary: Some(
"Virtual module needed for suspend/sleep when last module".to_string(),
),
mock: None,
retry: None,
sleep: None,
suspend: None,
+69 -36
View File
@@ -1225,43 +1225,64 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
let mut transform_context: Option<IdContext> = None;
let args: windmill_common::error::Result<_> = match &module.value {
FlowModuleValue::Script { input_transforms, .. }
| FlowModuleValue::RawScript { input_transforms, .. }
| FlowModuleValue::Flow { input_transforms, .. } => {
let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?;
transform_context = Some(ctx);
let by_id = transform_context.as_ref().unwrap();
transform_input(
&flow_job.args,
last_result.clone(),
input_transforms,
resume_messages.as_slice(),
approvers,
by_id,
client,
)
.await
}
FlowModuleValue::Identity => match last_result.clone() {
Value::Object(m) => Ok(m),
v @ _ => {
let mut m = Map::new();
m.insert("previous_result".to_string(), v);
Ok(m)
let args: windmill_common::error::Result<_> = if module.mock.is_some()
&& module.mock.as_ref().unwrap().enabled
{
let mut m = Map::new();
let v = module
.mock
.as_ref()
.unwrap()
.return_value
.clone()
.ok_or_else(|| {
Error::BadRequest(format!(
"mock enabled but no return_value specified for module {}",
module.id
))
})?;
m.insert("previous_result".to_string(), v);
Ok(m)
} else {
match &module.value {
FlowModuleValue::Script { input_transforms, .. }
| FlowModuleValue::RawScript { input_transforms, .. }
| FlowModuleValue::Flow { input_transforms, .. } => {
let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?;
transform_context = Some(ctx);
let by_id = transform_context.as_ref().unwrap();
transform_input(
&flow_job.args,
last_result.clone(),
input_transforms,
resume_messages.as_slice(),
approvers,
by_id,
client,
)
.await
}
},
_ => {
/* embedded flow input is augmented with embedding flow input */
if let Some(value) = &flow_job.args {
Ok(value
.as_object()
.ok_or_else(|| {
Error::BadRequest(format!("Expected an object value, found: {value:?}"))
})?
.clone())
} else {
Ok(Map::new())
FlowModuleValue::Identity => match last_result.clone() {
Value::Object(m) => Ok(m),
v @ _ => {
let mut m = Map::new();
m.insert("previous_result".to_string(), v);
Ok(m)
}
},
_ => {
/* embedded flow input is augmented with embedding flow input */
if let Some(value) = &flow_job.args {
Ok(value
.as_object()
.ok_or_else(|| {
Error::BadRequest(format!("Expected an object value, found: {value:?}"))
})?
.clone())
} else {
Ok(Map::new())
}
}
}
};
@@ -1648,6 +1669,18 @@ async fn compute_next_flow_transform<'c>(
previous_id: String,
client: &AuthedClient,
) -> error::Result<(sqlx::Transaction<'c, sqlx::Postgres>, NextFlowTransform)> {
if module.mock.is_some() && module.mock.as_ref().unwrap().enabled {
return Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::Identity,
tag: None,
}),
NextStatus::NextStep,
),
));
}
match &module.value {
FlowModuleValue::Identity => Ok((
tx,
@@ -11,6 +11,7 @@
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
import FlowModuleSleep from './FlowModuleSleep.svelte'
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
import FlowModuleMock from './FlowModuleMock.svelte'
export let flowModule: FlowModule
export let previousModule: FlowModule | undefined
@@ -67,6 +68,7 @@
<Tab value="early-stop">Early Stop/Break</Tab>
<Tab value="suspend">Suspend/Approval</Tab>
<Tab value="sleep">Sleep</Tab>
<Tab value="mock">Mock</Tab>
<svelte:fragment slot="content">
<div class="overflow-hidden bg-white">
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
@@ -84,6 +86,11 @@
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
</div>
</TabContent>
<TabContent value="mock" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleMock bind:flowModule />
</div>
</TabContent>
</div>
</svelte:fragment>
</Tabs>
@@ -11,6 +11,7 @@
import FlowModuleSleep from './FlowModuleSleep.svelte'
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
import SplitPanesWrapper from '../../splitPanes/SplitPanesWrapper.svelte'
import FlowModuleMock from './FlowModuleMock.svelte'
// import FlowRetries from './FlowRetries.svelte'
export let flowModule: FlowModule
@@ -67,6 +68,7 @@
<Tab value="early-stop">Early Stop/Break</Tab>
<Tab value="suspend">Suspend/Approval</Tab>
<Tab value="sleep">Sleep</Tab>
<Tab value="mock">Mock</Tab>
<svelte:fragment slot="content">
<div class="overflow-hidden bg-white">
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
@@ -84,6 +86,11 @@
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
</div>
</TabContent>
<TabContent value="mock" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleMock bind:flowModule />
</div>
</TabContent>
</div>
</svelte:fragment>
</Tabs>
@@ -15,6 +15,7 @@
import { getStepPropPicker } from '../previousResults'
import FlowModuleSleep from './FlowModuleSleep.svelte'
import FlowModuleMock from './FlowModuleMock.svelte'
const { previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -113,6 +114,7 @@
<Tab value="early-stop">Early Stop/Break</Tab>
<Tab value="suspend">Suspend/Approval</Tab>
<Tab value="sleep">Sleep</Tab>
<Tab value="mock">Mock</Tab>
<svelte:fragment slot="content">
<div class="overflow-hidden bg-white" style="height:calc(100% - 32px);">
@@ -138,6 +140,11 @@
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule={mod} />
</div>
</TabContent>
<TabContent value="mock" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleMock bind:flowModule={mod} />
</div>
</TabContent>
</div>
</svelte:fragment>
</Tabs>
@@ -29,6 +29,7 @@
import FlowPathViewer from './FlowPathViewer.svelte'
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
import { schemaToObject } from '$lib/schema'
import FlowModuleMock from './FlowModuleMock.svelte'
const { selectedId, previewArgs, flowStateStore, flowStore, saveDraft } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -133,6 +134,7 @@
bind:module={flowModule}
on:toggleSuspend={() => selectAdvanced('suspend')}
on:toggleSleep={() => selectAdvanced('sleep')}
on:toggleMock={() => selectAdvanced('mock')}
on:toggleRetry={() => selectAdvanced('retries')}
on:toggleCache={() => selectAdvanced('cache')}
on:toggleStopAfterIf={() => selectAdvanced('early-stop')}
@@ -265,6 +267,7 @@
<Tab value="early-stop">Early Stop/Break</Tab>
<Tab value="suspend">Suspend/Approval</Tab>
<Tab value="sleep">Sleep</Tab>
<Tab value="mock">Mock</Tab>
<Tab value="same_worker">Shared Directory</Tab>
{/if}
</Tabs>
@@ -285,6 +288,10 @@
<div>
<FlowModuleCache bind:flowModule />
</div>
{:else if advancedSelected === 'mock'}
<div>
<FlowModuleMock bind:flowModule />
</div>
{:else if advancedSelected === 'same_worker'}
<div>
<Alert type="info" title="Share a directory between steps">
@@ -3,7 +3,7 @@
import { WorkerService, type FlowModule } from '$lib/gen'
import { faCodeBranch, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher, getContext } from 'svelte'
import { Bed, Database, PhoneIncoming, Repeat, Square } from 'lucide-svelte'
import { Bed, Database, PhoneIncoming, Repeat, Square, Voicemail } from 'lucide-svelte'
import Popover from '../../Popover.svelte'
import type { FlowEditorContext } from '../types'
import { sendUserToast } from '$lib/utils'
@@ -83,6 +83,17 @@
<Bed size={14} />
<svelte:fragment slot="text">Sleep</svelte:fragment>
</Popover>
<Popover
placement="bottom"
class="center-center rounded p-2
{module.mock?.enabled
? 'bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200'
: 'bg-white text-gray-800 hover:bg-gray-100'}"
on:click={() => dispatch('toggleMock')}
>
<Voicemail size={14} />
<svelte:fragment slot="text">Mock</svelte:fragment>
</Popover>
{/if}
{#if module.value.type === 'script'}
<div class="w-2" />
@@ -0,0 +1,57 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import type { FlowModule } from '$lib/gen'
import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
export let flowModule: FlowModule
let code: string | undefined = flowModule.mock?.return_value
? JSON.stringify(flowModule.mock?.return_value, null, 2)
: undefined
$: isMockEnabled = Boolean(flowModule.mock?.enabled)
</script>
<h2 class="pb-4">
Mock
<Tooltip>
If defined and enabled, the step will immediately return the mock value instead of being
executed.
</Tooltip>
</h2>
<Toggle
checked={isMockEnabled}
on:change={() => {
if (isMockEnabled) {
flowModule.mock = {
enabled: false,
return_value: flowModule.mock?.return_value
}
} else {
flowModule.mock = {
enabled: true,
return_value: flowModule.mock?.return_value ?? { example: 'value' }
}
code = JSON.stringify(flowModule.mock?.return_value, null, 2)
}
console.log(isMockEnabled, flowModule.mock)
}}
options={{
right: 'Enable step mocking'
}}
/>
<div>
<span class="text-xs font-bold">Mocked Return value</span>
{#if flowModule?.mock?.return_value != undefined}
<JsonEditor {code} bind:value={flowModule.mock.return_value} />
{:else}
<input
type="text"
disabled
value={flowModule.mock?.return_value ? code : ''}
class="w-full p-2 border rounded-md"
/>
{/if}
</div>
@@ -2,7 +2,7 @@
import Badge from '$lib/components/common/badge/Badge.svelte'
import Popover from '$lib/components/Popover.svelte'
import { classNames } from '$lib/utils'
import { Bed, Database, Move, PhoneIncoming, Repeat, Square, X } from 'lucide-svelte'
import { Bed, Database, Move, PhoneIncoming, Repeat, Square, Voicemail, X } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
@@ -13,6 +13,7 @@
export let earlyStop: boolean = false
export let suspend: boolean = false
export let sleep: boolean = false
export let mock: boolean = false
export let bold: boolean = false
export let id: string | undefined = undefined
export let label: string
@@ -88,6 +89,17 @@
<svelte:fragment slot="text">Sleep</svelte:fragment>
</Popover>
{/if}
{#if mock}
<Popover notClickable>
<div
transition:fade|local={{ duration: 200 }}
class="center-center bg-white rounded border border-gray-400 text-gray-700 px-1 py-0.5"
>
<Voicemail size={14} />
</div>
<svelte:fragment slot="text">Mocked</svelte:fragment>
</Popover>
{/if}
</div>
<div
class="flex gap-1 justify-between items-center w-full overflow-hidden rounded-sm
@@ -42,7 +42,8 @@
earlyStop: mod.stop_after_if != undefined,
suspend: Boolean(mod.suspend),
sleep: Boolean(mod.sleep),
cache: Boolean(mod.cache_ttl)
cache: Boolean(mod.cache_ttl),
mock: Boolean(mod.mock?.enabled)
}
function onDelete(event: CustomEvent<MouseEvent>) {
+6
View File
@@ -92,6 +92,12 @@ components:
type: number
summary:
type: string
mock:
type: object
properties:
enabled:
type: boolean
return_value: {}
suspend:
type: object
properties: