Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display error banner when Reports server returns status code ≠ 2xx #1608

Merged
merged 40 commits into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d115c51
Add scenario displaying expected result
vincent-psarga Mar 9, 2021
513be7e
Display error returned by the server instead of stack trace
vincent-psarga Mar 9, 2021
e8fa74a
Make cucumber-js fail when an error is returned byt the server
vincent-psarga Mar 10, 2021
7ec1932
Investigate failing test on CI
vincent-psarga Mar 10, 2021
a4e1fd5
Get it working with node 14
vincent-psarga Mar 10, 2021
74b7017
Try getting this working on other node versions than 14 and 15
vincent-psarga Mar 10, 2021
096558b
Rearrange steps like other scenarios
vincent-psarga Mar 10, 2021
ff59a87
Disable fail-fast for CI
vincent-psarga Mar 11, 2021
0c4bf08
Remove comment in CI config
vincent-psarga Mar 11, 2021
6ad6421
Make error first arg in HttpStream to match conventions
aurelien-reeves Mar 11, 2021
2dddb53
Transform HttpStream from Writable to Transform
aurelien-reeves Mar 11, 2021
e85eb8b
Get one more test passing with new Stream
vincent-psarga Mar 11, 2021
a525da9
Refactoring. Next up: Remove sendRequest recursion
Mar 11, 2021
2352da8
Refactor HttpStream to avoid recursion
vincent-psarga Mar 12, 2021
5af9935
fix scenario
vincentcapicotto Mar 12, 2021
c5dfdb8
Try using a single hook to avoid potential race conditions
vincent-psarga Mar 12, 2021
4af0bdf
Try getting more infos about failing tests (maybe)
vincent-psarga Mar 12, 2021
b111591
Linting ...
vincent-psarga Mar 12, 2021
ca39fa7
Move logging, just in case ... (yes the test won't fail anymore ...)
vincent-psarga Mar 12, 2021
1bc0165
More debugging
vincent-psarga Mar 12, 2021
aaab859
Simplify HttpStream implementation
Mar 12, 2021
23b7cfe
Expect HTTP error in output
Mar 12, 2021
14a0b1d
Fix typo
Mar 12, 2021
36ca4ff
Add debugging
Mar 12, 2021
77210df
Add other debug traces
aurelien-reeves Mar 12, 2021
f58e3e9
Add other debug traces, also on fake server
aurelien-reeves Mar 12, 2021
f199e3e
Use progress formatter in CI
Mar 15, 2021
d3ab863
add fuzz test about "premature close"
vincentcapicotto Mar 15, 2021
39c47a5
increase race condition test rerun
vincentcapicotto Mar 15, 2021
9ad0126
Test if a simple delay between tests can avoid stream issues
vincent-psarga Mar 15, 2021
6ba0079
Another tests for delay between tests
vincent-psarga Mar 15, 2021
ed8e58e
Linting ...
vincent-psarga Mar 15, 2021
ebecb2b
Remove tests with timeout
vincent-psarga Mar 15, 2021
d4c6970
Add a missing await in FakeReportServer
aurelien-reeves Mar 16, 2021
b50fcd1
Remove useless await in FakeReportServer
aurelien-reeves Mar 16, 2021
1af43ce
Attempt to avoid race condition in FakeReportServer
aurelien-reeves Mar 16, 2021
7a62bb5
Delay server response in test
Mar 16, 2021
3272840
Use a (new) random port for each test
Mar 16, 2021
36666d3
Remove delays and logging
Mar 16, 2021
2e744ae
Remove test to reproduce race condition error
aurelien-reeves Mar 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
- ubuntu-latest
- windows-latest
node-version: [10.x, 12.x, 14.x, 15.x]
fail-fast: false
vincent-psarga marked this conversation as resolved.
Show resolved Hide resolved

steps:
- uses: actions/checkout@v2
- name: with Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO
### Fixed

* Fix types for hook functions so they can return e.g. `'skipped'` ([#1542](https://github.com/cucumber/cucumber-js/pull/1542))
* Display the response of the reports server when an error is returned before failing. ([#1608](https://github.com/cucumber/cucumber-js/pull/1608))

## [7.0.0] (2020-12-21)

Expand Down
4 changes: 1 addition & 3 deletions cucumber.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const feature = [
'--require-module ts-node/register',
'--require features/**/*.ts',
`--format ${
process.env.CI || !process.stdout.isTTY ? 'progress' : 'progress-bar'
}`,
`--format progress-bar`,
'--format rerun:@rerun.txt',
'--format usage:usage.txt',
'--format message:messages.ndjson',
Expand Down
26 changes: 20 additions & 6 deletions features/publish.feature
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Feature: Publish reports

@spawn
Scenario: Report is published when CUCUMBER_PUBLISH_TOKEN is set
When I run cucumber-js with arguments `` and env `CUCUMBER_PUBLISH_TOKEN=keyboardcat`
When I run cucumber-js with arguments `` and env `CUCUMBER_PUBLISH_TOKEN=f318d9ec-5a3d-4727-adec-bd7b69e2edd3`
Then it passes
And the server should receive the following message types:
| meta |
Expand All @@ -69,7 +69,7 @@ Feature: Publish reports
| testStepFinished |
| testCaseFinished |
| testRunFinished |
And the server should receive an "Authorization" header with value "Bearer keyboardcat"
And the server should receive an "Authorization" header with value "Bearer f318d9ec-5a3d-4727-adec-bd7b69e2edd3"

@spawn
Scenario: a banner is displayed after publication
Expand Down Expand Up @@ -101,6 +101,20 @@ Feature: Publish reports
│ module.exports = { default: '--publish-quiet' } │
└──────────────────────────────────────────────────────────────────────────┘
"""

@spawn
Scenario: when results are not published due to an error raised by the server, the banner is displayed
When I run cucumber-js with env `CUCUMBER_PUBLISH_TOKEN=keyboardcat`
Then it fails
davidjgoss marked this conversation as resolved.
Show resolved Hide resolved
And the error output contains the text:
"""
┌─────────────────────┐
│ Error invalid token │
└─────────────────────┘

Unexpected http status 401 from GET http://localhost:9987
"""

@spawn
Scenario: the publication banner is not shown when publication is done
When I run cucumber-js with arguments `<args>` and env `<env>`
Expand All @@ -110,10 +124,10 @@ Feature: Publish reports
"""

Examples:
| args | env |
| --publish | |
| | CUCUMBER_PUBLISH_ENABLED=true |
| | CUCUMBER_PUBLISH_TOKEN=123456 |
| args | env |
| --publish | |
| | CUCUMBER_PUBLISH_ENABLED=true |
| | CUCUMBER_PUBLISH_TOKEN=f318d9ec-5a3d-4727-adec-bd7b69e2edd3 |

@spawn
Scenario: the publication banner is not shown when publication is disabled
Expand Down
17 changes: 15 additions & 2 deletions features/step_definitions/cli_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ When(
}
)

When(
/^I run cucumber-js with env `(|.+)`$/,
{ timeout: 10000 },
async function (this: World, envString: string) {
const env = this.parseEnvString(envString)
return await this.run(this.localExecutablePath, [], env)
}
)

When(
/^I run cucumber-js with all formatters(?: and `(|.+)`)?$/,
{ timeout: 10000 },
Expand Down Expand Up @@ -67,10 +76,14 @@ When(
Then(/^it passes$/, () => {}) // eslint-disable-line @typescript-eslint/no-empty-function

Then(/^it fails$/, function (this: World) {
const actualCode = doesHaveValue(this.lastRun.error)
const actualCode: number = doesHaveValue(this.lastRun.error)
? this.lastRun.error.code
: 0
expect(actualCode).not.to.eql(0)

expect(actualCode).not.to.eql(
0,
`Expected non-zero exit status, but got ${actualCode}`
)
this.verifiedLastRunError = true
})

Expand Down
12 changes: 5 additions & 7 deletions features/support/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ Before('@global-install', function (this: World) {
)
})

After(function (this: World) {
After(async function (this: World) {
if (this.reportServer?.started) {
await this.reportServer.stop()
}

if (
doesHaveValue(this.lastRun) &&
doesHaveValue(this.lastRun.error) &&
Expand All @@ -106,9 +110,3 @@ After(function (this: World) {
)
}
})

After(async function (this: World) {
if (this.reportServer?.started) {
await this.reportServer.stop()
}
})
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@
},
"tsd": {
"compilerOptions": {
"types": ["long"]
"types": [
"long"
]
}
},
"bin": {
Expand Down
19 changes: 16 additions & 3 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import GherkinStreams from '@cucumber/gherkin/dist/src/stream/GherkinStreams'
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
import { IParsedArgvFormatOptions } from './argv_parser'
import HttpStream from '../formatter/http_stream'
import { Writable } from 'stream'

const { incrementing, uuid } = IdGenerator

Expand Down Expand Up @@ -98,14 +99,26 @@ export default class Cli {
headers.Authorization = `Bearer ${process.env.CUCUMBER_PUBLISH_TOKEN}`
}

stream = new HttpStream(outputTo, 'GET', headers, (content) =>
console.error(content)
)
stream = new HttpStream(outputTo, 'GET', headers)
const readerStream = new Writable({
objectMode: true,
write: function (responseBody: string, encoding, writeCallback) {
console.error(responseBody)
writeCallback()
},
})
stream.pipe(readerStream)
} else {
const fd = await fs.open(path.resolve(this.cwd, outputTo), 'w')
stream = fs.createWriteStream(null, { fd })
}
}

stream.on('error', (error) => {
console.error(error.message)
process.exit(1)
})

const typeOptions = {
cwd: this.cwd,
eventBroadcaster,
Expand Down
157 changes: 82 additions & 75 deletions src/formatter/http_stream.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { pipeline, Writable } from 'stream'
import { pipeline, Transform, Writable } from 'stream'
import tmp from 'tmp'
import fs from 'fs'
import http from 'http'
import https from 'https'
import { doesHaveValue } from '../value_checker'

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
type HttpMethod =
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'TRACE'
| 'PATCH'
type HttpMethod = 'GET' | 'POST' | 'PUT'

/**
* This Writable writes data to a HTTP/HTTPS URL.
Expand All @@ -27,18 +17,18 @@ type HttpMethod =
*
* 3xx redirects are not currently followed.
*/
export default class HttpStream extends Writable {
export default class HttpStream extends Transform {
private tempFilePath: string
private tempFile: Writable
private responseBodyFromGet: string | null = null

constructor(
private readonly url: string,
private readonly method: HttpMethod,
private readonly headers: { [name: string]: string },
private readonly reportLocation: (content: string) => void
private readonly headers: http.OutgoingHttpHeaders
) {
super()
super({
readableObjectMode: true,
})
}

_write(
Expand All @@ -49,7 +39,6 @@ export default class HttpStream extends Writable {
if (this.tempFile === undefined) {
tmp.file((err, name, fd) => {
if (doesHaveValue(err)) return callback(err)

this.tempFilePath = name
this.tempFile = fs.createWriteStream(name, { fd })
this.tempFile.write(chunk, encoding, callback)
Expand All @@ -61,79 +50,97 @@ export default class HttpStream extends Writable {

_final(callback: (error?: Error | null) => void): void {
this.tempFile.end(() => {
this.sendRequest(
this.sendHttpRequest(
this.url,
this.method,
(err: Error | null | undefined) => {
if (doesHaveValue(err)) return callback(err)
this.reportLocation(this.responseBodyFromGet)
callback(null)
this.headers,
(err1, res1) => {
if (doesHaveValue(err1)) return callback(err1)
this.pushResponseBody(res1, () => {
this.emitErrorUnlessHttp2xx(res1, this.url, this.method)
if (
res1.statusCode === 202 &&
res1.headers.location !== undefined
) {
this.sendHttpRequest(
res1.headers.location,
'PUT',
{},
(err2, res2) => {
if (doesHaveValue(err2)) return callback(err2)
this.emitErrorUnlessHttp2xx(res2, this.url, this.method)
callback()
}
)
} else {
callback()
}
})
}
)
})
}

private sendRequest(
private pushResponseBody(res: http.IncomingMessage, done: () => void): void {
let body = Buffer.alloc(0)
res.on('data', (chunk) => {
body = Buffer.concat([body, chunk])
})
res.on('end', () => {
this.push(body.toString('utf-8'))
done()
})
}

private emitErrorUnlessHttp2xx(
res: http.IncomingMessage,
url: string,
method: string
): void {
if (res.statusCode >= 300)
this.emit(
'error',
new Error(
`Unexpected http status ${res.statusCode} from ${method} ${url}`
)
)
}

private sendHttpRequest(
url: string,
method: HttpMethod,
callback: (err: Error | null | undefined, url?: string) => void
headers: http.OutgoingHttpHeaders,
callback: (err?: Error | null, res?: http.IncomingMessage) => void
): void {
const httpx = doesHaveValue(url.match(/^https:/)) ? https : http
const additionalHttpHeaders: http.OutgoingHttpHeaders = {}

if (method === 'GET') {
httpx.get(url, { headers: this.headers }, (res) => {
if (res.statusCode >= 400) {
return callback(
new Error(`${method} ${url} returned status ${res.statusCode}`)
)
}

if (res.statusCode !== 202 || res.headers.location === undefined) {
callback(null, url)
} else {
let body = Buffer.alloc(0)
res.on('data', (chunk) => {
body = Buffer.concat([body, chunk])
})
res.on('end', () => {
this.responseBodyFromGet = body.toString('utf-8')
this.sendRequest(res.headers.location, 'PUT', callback)
})
}
})
} else {
const contentLength = fs.statSync(this.tempFilePath).size
const req = httpx.request(url, {
method,
headers: {
'Content-Length': contentLength,
},
})
const upload = method === 'PUT' || method === 'POST'
if (upload) {
additionalHttpHeaders['Content-Length'] = fs.statSync(
this.tempFilePath
).size
}

req.on('response', (res) => {
if (res.statusCode >= 400) {
let body = Buffer.alloc(0)
res.on('data', (chunk) => {
body = Buffer.concat([body, chunk])
})
res.on('end', () => {
callback(
new Error(
`${method} ${url} returned status ${
res.statusCode
}:\n${body.toString('utf-8')}`
)
)
})
res.on('error', callback)
} else {
callback(null, url)
}
})
const allHeaders = { ...headers, ...additionalHttpHeaders }
const req = httpx.request(url, {
method,
headers: allHeaders,
})
req.on('error', (err) => this.emit('error', err))
req.on('response', (res) => {
res.on('error', (err) => this.emit('error', err))
callback(null, res)
})

if (upload) {
pipeline(fs.createReadStream(this.tempFilePath), req, (err) => {
if (doesHaveValue(err)) callback(err)
if (doesHaveValue(err)) {
this.emit('error', err)
}
})
} else {
req.end()
}
}
}
Loading