Skip to content

Commit 6ff214e

Browse files
authored
feat: support build tag detection (#379)
* fix: require WASM env for bench and fuzz * chore: rely on auto env switch * feat: add self-destruct notifications * feat: support build tag analysis
1 parent 99abfb9 commit 6ff214e

File tree

12 files changed

+95
-22
lines changed

12 files changed

+95
-22
lines changed

web/public/examples/templates/bench_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
//go:build wasm
2+
13
package main
24

35
import "testing"
46

7+
// **Important:** This benchmark will be executed using WebAssembly as original Go Playground API doesn't support benchmarks and fuzzing.
8+
// See: https://stackoverflow.com/questions/54574814/go-benchmark-run-from-main-go-playground
9+
510
func doJob() {
611
// Put here code to benchmark
712
}

web/public/examples/testing/bench_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build wasm
2+
13
// The Go testing package contains a benchmarking facility
24
// that can be used to examine the performance of your Go code.
35
//
@@ -6,6 +8,9 @@
68
// See: https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go
79
package main
810

11+
// **Important:** This benchmark will be executed using WebAssembly as original Go Playground API doesn't support benchmarks and fuzzing.
12+
// See: https://stackoverflow.com/questions/54574814/go-benchmark-run-from-main-go-playground
13+
914
import "testing"
1015

1116
// In this example we’re going to benchmark the speed of computing the 10th number in the Fibonacci series.

web/public/examples/testing/fuzz_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build wasm
2+
13
// This tutorial introduces the basics of fuzzing in Go.
24
// With fuzzing, random data is run against your test in an attempt to find vulnerabilities or crash-causing inputs.
35
// Some examples of vulnerabilities that can be found by fuzzing are SQL injection, buffer overflow,
@@ -12,6 +14,9 @@ import (
1214
"unicode/utf8"
1315
)
1416

17+
// **Important:** This test will be executed using WebAssembly as original Go Playground API doesn't support benchmarks and fuzzing.
18+
// See: https://stackoverflow.com/questions/54574814/go-benchmark-run-from-main-go-playground
19+
1520
// This function will accept a string, loop over it a byte at a time, and return the reversed string at the end.
1621
func Reverse(s string) (string, error) {
1722
if !utf8.ValidString(s) {

web/public/examples/wasm/main.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
// Go allows calling JavaScript functions from browser and also export functions to JavaScript.
77
package main
88

9-
///////////////////////////////////////////////////////////////////////////////////////////////////////
10-
// ⚠️ Attention: please select "WebAssembly" environment in top right corner.
11-
///////////////////////////////////////////////////////////////////////////////////////////////////////
12-
139
import (
1410
"fmt"
1511

web/src/lib/sourceutil/analysis.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const wasmBuildTagRegex = /^\/\/go:build\s(js|wasm)(\s(&&|\|\|)\s(js|wasm))?/
2+
3+
const isGoFile = (fileName: string) => fileName.endsWith('.go')
4+
5+
/**
6+
* Checks if Go program source files contain WebAssembly build constraints.
7+
*
8+
* For example: `//go:build wasm`
9+
*/
10+
export const requiresWasmEnvironment = (files: Record<string, string>): boolean =>
11+
Object.entries(files)
12+
.filter(([name]) => isGoFile(name))
13+
.some(([, src]) => src.match(wasmBuildTagRegex))

web/src/lib/sourceutil/flags.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
interface GoProgramInfo {
2+
isTest?: boolean
3+
hasBenchmark?: boolean
4+
hasFuzz?: boolean
5+
}
6+
7+
/**
8+
* Returns command line args for Go test binary based on server build response.
9+
*/
10+
export const buildGoTestFlags = ({ isTest, hasBenchmark, hasFuzz }: GoProgramInfo): string[] => {
11+
const flags: Array<[string, boolean | undefined]> = [
12+
['-test.v', isTest],
13+
['-test.bench=.', hasBenchmark],
14+
['-test.fuzz=.', hasFuzz],
15+
]
16+
17+
return flags.filter(([, keep]) => !!keep).map(([arg]) => arg)
18+
}

web/src/lib/sourceutil/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './flags'
2+
export * from './analysis'

web/src/store/dispatchers/build.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { TargetType } from '~/services/config'
22
import { getImportObject, goRun } from '~/services/go'
33
import { setTimeoutNanos, SECOND } from '~/utils/duration'
44
import { instantiateStreaming } from '~/lib/go'
5-
import client, { type BuildResponse, type EvalEvent, EvalEventKind } from '~/services/api'
5+
import { buildGoTestFlags, requiresWasmEnvironment } from '~/lib/sourceutil'
6+
import client, { type EvalEvent, EvalEventKind } from '~/services/api'
67
import { isProjectRequiresGoMod, goModFile, goModTemplate } from '~/services/examples'
78

89
import { type DispatchFn, type StateProvider } from '../helpers'
@@ -134,19 +135,6 @@ const fetchWasmWithProgress = async (dispatch: DispatchFn, fileName: string) =>
134135
}
135136
}
136137

137-
/**
138-
* Returns command line args for Go test binary based on server build response.
139-
*/
140-
const buildGoTestFlags = ({ isTest, hasBenchmark, hasFuzz }: BuildResponse): string[] => {
141-
const flags: Array<[string, boolean | undefined]> = [
142-
['-test.v', isTest],
143-
['-test.bench=.', hasBenchmark],
144-
['-test.fuzz=.', hasFuzz],
145-
]
146-
147-
return flags.filter(([, keep]) => !!keep).map(([arg]) => arg)
148-
}
149-
150138
export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getState: StateProvider) => {
151139
dispatch(newRemoveNotificationAction(NotificationIDs.WASMAppExitError))
152140
dispatch(newRemoveNotificationAction(NotificationIDs.GoModMissing))
@@ -155,7 +143,7 @@ export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getSta
155143
const {
156144
settings,
157145
workspace,
158-
runTarget: { target, backend },
146+
runTarget: { target: selectedTarget, backend },
159147
} = getState()
160148

161149
let { files, selectedFile } = workspace
@@ -205,7 +193,30 @@ export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getSta
205193
})
206194
}
207195

208-
switch (target) {
196+
// Force use WebAssembly for execution if source code contains go:build constraints.
197+
let runTarget = selectedTarget
198+
if (runTarget !== TargetType.WebAssembly && requiresWasmEnvironment(files)) {
199+
runTarget = TargetType.WebAssembly
200+
dispatch(
201+
newAddNotificationAction({
202+
id: NotificationIDs.GoTargetSwitched,
203+
type: NotificationType.Warning,
204+
title: 'Go environment temporarily changed',
205+
description: 'This program will be executed using WebAssembly as Go program contains "//go:build" tag.',
206+
canDismiss: true,
207+
actions: [
208+
{
209+
key: 'ok',
210+
label: 'Ok',
211+
primary: true,
212+
onClick: () => dispatch(newRemoveNotificationAction(NotificationIDs.GoTargetSwitched)),
213+
},
214+
],
215+
}),
216+
)
217+
}
218+
219+
switch (runTarget) {
209220
case TargetType.Server: {
210221
// TODO: vet
211222
const res = await client.run(files, false, backend)
@@ -239,7 +250,7 @@ export const runFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getSta
239250
break
240251
}
241252
default:
242-
dispatch(newErrorAction(`AppError: Unknown Go runtime type "${target}"`))
253+
dispatch(newErrorAction(`AppError: Unknown Go runtime type "${runTarget}"`))
243254
}
244255
} catch (err: any) {
245256
dispatch(newErrorAction(err.message))

web/src/store/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { type Action, type ActionType } from './actions'
2+
import type { Dispatcher } from './dispatchers'
23
import { type State } from './state'
34

45
export type Reducer<S, T> = (s: S, a: Action<T>) => S
56
export type ActionReducers<T> = { [k in keyof typeof ActionType | string]: Reducer<T, any> }
67

7-
export type DispatchFn = <T = any>(a: Action<T>) => any
8+
export type DispatchFn = <T = any>(a: Action<T> | Dispatcher) => any
89
export type StateProvider = () => State
910

1011
/**
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Dispatcher } from '../dispatchers'
2+
import type { DispatchFn, StateProvider } from '../helpers'
3+
import { newAddNotificationAction, newRemoveNotificationAction } from './actions'
4+
import type { Notification } from './state'
5+
6+
/**
7+
* Returns dispatcher that will show a notification which will disappear after a specified delay.
8+
*/
9+
export const dispatchNotificationWithTimeout = (notification: Notification, timeout = 5000): Dispatcher => {
10+
return (dispatch: DispatchFn, _: StateProvider) => {
11+
const { id } = notification
12+
setTimeout(() => dispatch(newRemoveNotificationAction(id)), timeout)
13+
dispatch(newAddNotificationAction(notification))
14+
}
15+
}

0 commit comments

Comments
 (0)