Skip to content

Commit 4ef9510

Browse files
authored
Merge pull request #23142 from mvdbeek/fix-flaky-testa
Stabilize flaky integration and UI tests
2 parents 61946a4 + 3e608b5 commit 4ef9510

38 files changed

Lines changed: 364 additions & 85 deletions

File tree

.github/workflows/integration.yaml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,57 @@ concurrency:
2525
cancel-in-progress: true
2626
permissions: {}
2727
jobs:
28+
prepare-mulled-cache:
29+
name: Prepare mulled resolution cache
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Select weekly cache
33+
run: echo "MULLED_CACHE_WEEK=$(date -u +%G-%V)" >> "$GITHUB_ENV"
34+
- name: Restore Biocontainers repository index
35+
id: restore-mulled-cache
36+
uses: actions/cache/restore@v6.1.0
37+
with:
38+
key: biocontainers-repository-index-${{ env.MULLED_CACHE_WEEK }}
39+
path: '${{ runner.temp }}/mulled-resolution-seed'
40+
- name: Check out Galaxy for mulled-cache
41+
if: steps.restore-mulled-cache.outputs.cache-hit != 'true'
42+
uses: actions/checkout@v7.0.0
43+
with:
44+
path: 'galaxy cache source'
45+
persist-credentials: false
46+
- name: Fetch Biocontainers repository index
47+
id: fetch-mulled-cache
48+
if: steps.restore-mulled-cache.outputs.cache-hit != 'true'
49+
continue-on-error: true
50+
env:
51+
SEED_DIRECTORY: '${{ runner.temp }}/mulled-resolution-seed'
52+
run: |
53+
python3 'galaxy cache source/lib/galaxy/tool_util/deps/mulled/mulled_cache.py' \
54+
--namespace biocontainers \
55+
--output "$SEED_DIRECTORY/biocontainers.json"
56+
- name: Save Biocontainers repository index
57+
if: steps.restore-mulled-cache.outputs.cache-hit != 'true' && steps.fetch-mulled-cache.outcome == 'success'
58+
uses: actions/cache/save@v6.1.0
59+
with:
60+
key: biocontainers-repository-index-${{ env.MULLED_CACHE_WEEK }}
61+
path: '${{ runner.temp }}/mulled-resolution-seed'
62+
- name: Create fallback seed
63+
if: steps.fetch-mulled-cache.outcome == 'failure'
64+
env:
65+
SEED_DIRECTORY: '${{ runner.temp }}/mulled-resolution-seed'
66+
run: |
67+
mkdir -p "$SEED_DIRECTORY"
68+
echo '{"namespace":"biocontainers","repositories":[]}' > "$SEED_DIRECTORY/biocontainers.json"
69+
- name: Upload mulled resolution cache seed
70+
uses: actions/upload-artifact@v7
71+
with:
72+
name: mulled-resolution-cache-seed
73+
path: '${{ runner.temp }}/mulled-resolution-seed/biocontainers.json'
74+
retention-days: 1
75+
2876
test:
2977
name: Test
78+
needs: prepare-mulled-cache
3079
runs-on: ubuntu-latest
3180
strategy:
3281
fail-fast: false
@@ -63,9 +112,18 @@ jobs:
63112
python-version: ${{ matrix.python-version }}
64113
- name: Install Python
65114
run: uv python install
115+
- name: Download mulled resolution cache seed
116+
uses: actions/download-artifact@v8
117+
with:
118+
name: mulled-resolution-cache-seed
119+
path: '${{ runner.temp }}/mulled-resolution-seed'
66120
- name: Install Apptainer's singularity
67121
uses: './galaxy root/.github/actions/install_apptainer'
68122
- name: Run tests
123+
env:
124+
GALAXY_TEST_MULLED_RESOLUTION_CACHE_SEED: '${{ runner.temp }}/mulled-resolution-seed/biocontainers.json'
125+
GALAXY_CONFIG_OVERRIDE_MULLED_RESOLUTION_CACHE_DATA_DIR: '${{ runner.temp }}/mulled-resolution-cache/data'
126+
GALAXY_CONFIG_OVERRIDE_MULLED_RESOLUTION_CACHE_LOCK_DIR: '${{ runner.temp }}/mulled-resolution-cache/locks'
69127
run: |
70128
. .ci/minikube-test-setup/start_services.sh
71129
./run_tests.sh --coverage -integration test/integration -- --num-shards=4 --shard-id=${{ matrix.chunk }}

client/packages/api-client/src/schema/schema.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20647,7 +20647,7 @@ export interface components {
2064720647
/** Version */
2064820648
version: string | null;
2064920649
/** Xrefs */
20650-
xrefs: components["schemas"]["XrefDict-Output"][];
20650+
xrefs: components["schemas"]["XrefDict"][];
2065120651
};
2065220652
/** ParsedWorkbook */
2065320653
ParsedWorkbook: {
@@ -26412,7 +26412,7 @@ export interface components {
2641226412
*/
2641326413
version: string;
2641426414
/** xrefs */
26415-
xrefs?: components["schemas"]["XrefDict-Input"][] | null;
26415+
xrefs?: components["schemas"]["XrefDict"][] | null;
2641626416
};
2641726417
/**
2641826418
* UserToolSource
@@ -26519,7 +26519,7 @@ export interface components {
2651926519
*/
2652026520
version: string;
2652126521
/** xrefs */
26522-
xrefs?: components["schemas"]["XrefDict-Output"][] | null;
26522+
xrefs?: components["schemas"]["XrefDict"][] | null;
2652326523
};
2652426524
/** UserUpdatePayload */
2652526525
UserUpdatePayload: {
@@ -27695,19 +27695,12 @@ export interface components {
2769527695
target_uri: string;
2769627696
};
2769727697
/** XrefDict */
27698-
"XrefDict-Input": {
27698+
XrefDict: {
2769927699
/** type */
2770027700
type: string;
2770127701
/** value */
2770227702
value: string;
2770327703
};
27704-
/** XrefDict */
27705-
"XrefDict-Output": {
27706-
/** Type */
27707-
type: string;
27708-
/** Value */
27709-
value: string;
27710-
};
2771127704
/** XrefItem */
2771227705
XrefItem: {
2771327706
/**
@@ -28330,7 +28323,7 @@ export interface components {
2833028323
*/
2833128324
version?: string | null;
2833228325
/** xrefs */
28333-
xrefs?: components["schemas"]["XrefDict-Input"][] | null;
28326+
xrefs?: components["schemas"]["XrefDict"][] | null;
2833428327
};
2833528328
/**
2833628329
* YamlToolTest

client/src/components/AvailableDatatypes/AvailableDatatypes.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const filter = ref("");
1313
const filterFields: Array<keyof DetailedDatatypes> = ["extension"];
1414
1515
const { datatypes } = useDetailedDatatypes();
16-
const filteredDatatypes = useFilterObjectArray(datatypes, filter, filterFields);
16+
const { filtered: filteredDatatypes } = useFilterObjectArray(datatypes, filter, filterFields);
1717
1818
const fields: TableField[] = [
1919
{

client/src/components/Form/Elements/FormSelect.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ const emit = defineEmits<{
5858
}>();
5959
6060
const filter = ref("");
61-
const filteredOptions = useFilterObjectArray(() => props.options, filter, ["label", ["value", "tags"]]);
61+
const { filtered: filteredOptions, pending: filterPending } = useFilterObjectArray(() => props.options, filter, [
62+
"label",
63+
["value", "tags"],
64+
]);
6265
6366
// Debounced upward emit so consumers (e.g. ``FormData`` paginating against the
6467
// backend) can refetch on typing without firing on every keystroke. The local
@@ -241,6 +244,7 @@ function isSelected(item: SelectValue): boolean {
241244
v-if="hasOptions"
242245
:id="id"
243246
v-model="currentValue"
247+
:data-filter-pending="filterPending ? 'true' : undefined"
244248
:allow-empty="optional || multiple"
245249
:aria-expanded="ariaExpanded"
246250
:close-on-select="!multiple"
@@ -258,7 +262,10 @@ function isSelected(item: SelectValue): boolean {
258262
@open="onOpen"
259263
@close="onClose">
260264
<template v-slot:option="{ option }">
261-
<div class="d-flex align-items-center justify-content-between">
265+
<!-- Replace recycled option content when its identity changes. -->
266+
<div
267+
:key="`${option.label}:${String(option.value)}`"
268+
class="d-flex align-items-center justify-content-between">
262269
<div>
263270
<span>{{ option.label }}</span>
264271
<StatelessTags

client/src/components/ToolsList/ToolsListTable.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ async function loadTools(offset: number, limit: number): Promise<{ items: Tool[]
4242
return { items, total: props.tools.length };
4343
}
4444
45-
// Force ScrollList remount when tools change (e.g. after search),
46-
// ensuring loadTools is called again to await help data for new results.
47-
const toolsKey = computed(() => `${props.tools.length}-${props.tools[0]?.id}`);
45+
// Remount ScrollList when its result set changes so it reloads help data.
46+
const toolsKey = computed(() => JSON.stringify(props.tools.map((tool) => tool.id)));
4847
</script>
4948

5049
<template>

client/src/components/Workflow/Editor/Node.vue

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
:class="headerClass"
2121
@pointerdown.exact="onPointerDown"
2222
@pointerup.exact="onPointerUp"
23+
@dblclick.exact="onDoubleClick"
2324
@click.shift.capture.prevent.stop="toggleSelected"
2425
@keyup.enter="makeActive">
2526
<b-button-group class="float-right">
@@ -104,6 +105,7 @@
104105
class="node-error m-0 rounded-0 rounded-bottom"
105106
@pointerdown.exact="onPointerDown"
106107
@pointerup.exact="onPointerUp"
108+
@dblclick.exact="onDoubleClick"
107109
@click.shift.capture.prevent.stop="toggleSelected">
108110
{{ errors }}
109111
</b-alert>
@@ -114,6 +116,7 @@
114116
:class="{ 'cursor-pointer': isInvocation || isPopulatedInput }"
115117
@pointerdown.exact="onPointerDown"
116118
@pointerup.exact="onPointerUp"
119+
@dblclick.exact="onDoubleClick"
117120
@click.shift.capture.prevent.stop="toggleSelected"
118121
@keyup.enter="makeActive">
119122
<NodeInput
@@ -352,10 +355,8 @@ function onDragConnector(dragPosition: TerminalPosition, terminal: OutputTermina
352355
353356
const mouseMovementThreshold = 9;
354357
const singleClickTimeout = 800;
355-
const doubleClickTimeout = 500;
356358
357359
let mouseDownTime = 0;
358-
let doubleClickTime = 0;
359360
360361
let movementDistance = 0;
361362
let lastPosition: XYPosition | null = null;
@@ -381,15 +382,19 @@ function onPointerUp(e: PointerEvent) {
381382
makeActive();
382383
}
383384
384-
const timeBetweenClicks = mouseUpTime - doubleClickTime;
385+
lastPosition = null;
386+
movementDistance = 0;
387+
}
388+
389+
function onDoubleClick(e: MouseEvent) {
390+
const path = composedPartialPath(e);
391+
const unclickable = path.every((target) => !isClickable(target as Element));
385392
386-
if (timeBetweenClicks < doubleClickTimeout) {
387-
inspectorStore.setMaximized(props.step, true);
393+
if (!unclickable) {
394+
return;
388395
}
389396
390-
doubleClickTime = Date.now();
391-
lastPosition = null;
392-
movementDistance = 0;
397+
inspectorStore.setMaximized(props.step, true);
393398
}
394399
395400
function onMoveTo(position: XYPosition) {

client/src/components/admin/RoleForm.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ vi.mock("vue-router/composables", () => ({
2121
vi.mock("@/composables/filter/filter.js", () => {
2222
const { ref } = require("vue");
2323
return {
24-
useFilterObjectArray: () => ref([]),
24+
useFilterObjectArray: () => ({ filtered: ref([]), pending: ref(false) }),
2525
};
2626
});
2727

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { toValue } from "@vueuse/core";
2-
import { computed, type Ref } from "vue";
2+
import { computed, ref } from "vue";
33

44
import type { useFilterObjectArray as UseFilterObjectArray } from "@/composables/filter";
55

@@ -8,6 +8,6 @@ vi.mock("@/composables/filter", () => ({
88
useFilterObjectArray,
99
}));
1010

11-
export const useFilterObjectArray: typeof UseFilterObjectArray = (array): Ref<any[]> => {
12-
return computed(() => toValue(array));
13-
};
11+
export const useFilterObjectArray = ((array) => {
12+
return { filtered: computed(() => toValue(array)), pending: ref(false) };
13+
}) as typeof UseFilterObjectArray;

client/src/composables/filter/filter.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export declare function useFilterObjectArray<O extends object, K extends keyof O
1212
array: MaybeRefOrGetter<Array<O>>,
1313
filter: MaybeRefOrGetter<string>,
1414
objectFields: MaybeRefOrGetter<Array<K | string[]>>,
15-
): Ref<O[]>;
15+
): { filtered: Ref<O[]>; pending: Ref<boolean> };

client/src/composables/filter/filter.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ export function useFilterObjectArray(array, filter, objectFields, asRegex = fals
77
const filtered = ref([]);
88
filtered.value = toValue(array);
99

10+
// Track the latest request so consumers never act on an intermediate result.
11+
const pending = ref(true);
12+
let sentSeq = 0;
13+
1014
const post = (message) => {
11-
worker.postMessage(message);
15+
sentSeq += 1;
16+
pending.value = true;
17+
worker.postMessage({ ...message, seq: sentSeq });
1218
};
1319

1420
watch(
@@ -44,14 +50,15 @@ export function useFilterObjectArray(array, filter, objectFields, asRegex = fals
4450
worker.onmessage = (e) => {
4551
const message = e.data;
4652

47-
if (message.type === "result") {
53+
if (message.type === "result" && message.seq === sentSeq) {
4854
filtered.value = message.filtered;
55+
pending.value = false;
4956
}
5057
};
5158

5259
onScopeDispose(() => {
5360
worker.terminate();
5461
});
5562

56-
return filtered;
63+
return { filtered, pending };
5764
}

0 commit comments

Comments
 (0)