Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ The `constants.yaml` file is *per-flavor*. In the file, you can define strings (
buildDir: 'build'
sourceDir: 'src'
preBuild: 'scripts/main.js'
runUnitTestsScript: 'scripts/runTests.js'
mainFlavor: 'flavorName'
flavors: {
flavorName: {
Expand Down Expand Up @@ -182,29 +183,42 @@ NOTE: you will need to define a `packageReference` and `packageKey` in `ukor.pro

## Testing

Ukor uses `UnitTestFramework.brs` as part of its unit test runner and test reporting feature. This is especially useful if you plan on having Continuous Integration as part of your workflow.
With ukor you have possibility to use whatever unit test framework you want !

Checkout ours [unit testing scripts](http://github.com/willowtreeapps/ukor/tree/master/unit-testing-scripts)

### Setup

First, copy the modified `UnitTestFramework.brs` in [lib/brs/](../master/lib/brs/UnitTestFramework.brs) to your `src/test/source/` folder, so it loads at startup for when testing.
#### 1. Define you custom tests running js script:

Example: `/scripts/runTests.js`
```
const child_process = require('child_process')

const args = process.argv.slice(2)
const ip = args[0] // the roku device ip given as an argument by ukor

// main function call
main()

function main() {
child_process.execSync(`npx rooibosC -c ./.rooibosrc.json`)
child_process.execSync(`curl -d '' "http://${ip}:8060/launch/dev?RunTests=true"`)
}
```

> Note that the original `UnitTestFramework.brs` can be found [here](https://github.com/rokudev/unit-testing-framework)
#### 2. Add the path of your script to ukor.properties.yaml

Next, dd the following snippet in your startup function, after `screen.show()` but before the event loop
```
...
buildDir: build
sourceDir: src
preBuild: scripts/preBuild.js

```brightscript
if params.RunTests = "true"
runner = TestRunner()
if params.host <> invalid
runner.logger.SetServer(params.host, params.port)
else
runner.logger.SetServerURL(params.url)
end if
' other setup if needed
runner.run()
end if
runUnitTestsScript: scripts/runTests.js
```


You should now be able to execute your test suite using the `test` command.

```
Expand All @@ -213,11 +227,14 @@ ukor test [flavor] [roku]

### What's happening?

Basically, we modified the rokudev `UnitTestFramework.brs` file to make a `JSON` of test results, and then `POST` that to the specified server. `ukor test [flavor]` builds and deploys the specified flavor with the `test` src folder, and then restarts the channel with parameters to run tests and point the results to the client machine. `ukor` will log the results, and also output results in `xml` and `junit` format to `.out/tests/ukorTests.[xml|junit]`.
After running the test command, ukor will deploy the channel on given device.
Once the channel is deployed, ukor starts to run the `runTests.js` script with the device `ip` as argument.

notes:
- Ukor now copies `UnitTestFramework.brs` with `ukor init`!
- `UnitTestFramework.brs` is now up to date with the rokudev repo!
You can get `ip` inside of `runTests.js` by:
```
const args = process.argv.slice(2)
const ip = args[0] // the roku device ip given as an argument by ukor
```

# Contributing to Ukor

Expand Down
28 changes: 1 addition & 27 deletions bin/ukor-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,13 @@ const yaml = require('js-yaml')

try {
fs.copySync(path.resolve(__dirname, '../lib/sample'), './', {
overwrite: false,
overwrite: false,
errorOnExist: false
})
log.info('created src directory')
} catch (e) {
log.error('failed to create src directory')
log.error(e)
}

const propsFile = './ukor.properties.yaml'
if (!fs.existsSync(propsFile)) {
const project = fs.createWriteStream(propsFile)
const defaults = {
name: path.basename(process.cwd()),
version: '0.0.1',
buildDir: 'build',
sourceDir: 'src',
defaults: {
flavor: 'main'
},
flavors: {
main: {
src: ['main']
}
}
}
project.write(yaml.safeDump(defaults), () => {
project.close()
log.info(`created ${propsFile}`)
})
} else {
log.info(`${propsFile} already exists`)
}

log.info('Ukor works greate with Wist, run "wist -i" to generate a default .wistrc.json')

20 changes: 5 additions & 15 deletions lib/commands/make.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const archiver = require('archiver')
const fs = require('fs')
const path = require('path')
const log = require('../utils/log')
const properties = require('../utils/properties')
const mkdirp = require('mkdirp')
const fse = require('fs-extra')
const rimraf = require('rimraf')
const log = require('../utils/log')
const archiver = require('archiver')
const utils = require('../utils/utils')
const inserter = require('../utils/inserter')
const childProcess = require('child_process')
const properties = require('../utils/properties')
const temp = '.ukor'

function moveToTempDir(flavor) {
Expand All @@ -31,18 +31,8 @@ function moveToTempDir(flavor) {
}
}

function execPreBuildScript() {
if (properties.preBuild != null && properties.preBuild != '') {
log.info('exec ' + properties.preBuild)
let result = childProcess.execFileSync('node', [properties.preBuild])
if (log.level == 'verbose') {
log.verbose([properties.preBuild, 'exec:', result].join(' '))
}
}
}

function bundleFlavor(options, callback) {
execPreBuildScript()
if (properties.preBuild) utils.execScript(properties.preBuild)

const flavor = options.flavor
let flavorSrc = properties.flavors[flavor].src
Expand Down
165 changes: 8 additions & 157 deletions lib/commands/test.js
Original file line number Diff line number Diff line change
@@ -1,169 +1,20 @@
const request = require('request')
const log = require('../utils/log')
const path = require('path')
const installer = require('./install')
const http = require('http')
const fs = require('fs')
const getIP = require('ip')
const XMLWriter = require('xml-writer')
const mkdirp = require('mkdirp')
const utils = require('../utils/utils')
const properties = require('../utils/properties')

function runLogServer(ip, port, timeout, callback) {
let logServer = http.createServer((req, res) => {
let body = []
req.on('data', data => {
body.push(data)
})
req.on('end', () => {
try {
body = Buffer.concat(body).toString()
let stats = JSON.parse(body)
writeJunit(stats)
if (stats) {
log.info('------------')
log.info('Test Results')
log.info(
'Pass: %d | Fail: %d | Crash: %d',
stats.correct,
stats.fail,
stats.crash
)
log.info('===================================')
stats.suites.forEach(suite => {
log.info('%s: %d tests', suite.name, suite.tests.length)
suite.tests.forEach(test => {
log.info('\t%s: %s', test.name, test.result)
if (test.result != 'Success') {
log.info(
'\t\tError %d: %s',
test.error.code,
test.error.message
)
}
})
log.info('---------------------------------')
})
log.info(
'Pass: %d | Fail: %d | Crash: %d',
stats.correct,
stats.fail,
stats.crash
)
if (stats.fail > 0) {
process.exit(-1)
}
}
callback ? callback(stats) : null
res.statusCode = '200'
} catch (e) {
log.error(e.message)
res.statusCode = '403'
}
res.end('ok', () => {
logServer.close()
})
})
})
logServer.listen(parseInt(port), getIP.address(), () => {
log.info('listening for tests on %s:%s', getIP.address(), port)
})
return logServer
function runTestScript(script, execArgs) {
log.info(`Running ${script} script with args ${execArgs}...`)
utils.execScript(script, execArgs)
}

function writeJunit(stats) {
log.info('building junit xml')
const xml = new XMLWriter()
xml.startDocument()
xml
.startElement('testsuites')
.writeAttribute('name', 'rokuTests')
.writeAttribute('tests', '' + stats.total)
.writeAttribute('failures', '' + stats.fail)
stats.suites.forEach(suite => {
xml
.startElement('testsuite')
.writeAttribute('name', suite.name)
.writeAttribute('tests', '' + suite.total)
.writeAttribute('failures', suite.fail)
.writeAttribute('skipped', suite.skipped)
suite.tests.forEach(test => {
xml
.startElement('testcase')
.writeAttribute('name', test.name)
.writeAttribute('time', '' + test.time * 0.001)
if (test.result.toLowerCase() === 'fail') {
xml
.startElement('failure')
.writeAttribute('type', '' + test.error.code)
.text(test.error.message)
.endElement()
} else if (test.result.toLowerCase() === 'skipped') {
xml
.startElement('skipped')
.endElement()
}
xml.endElement()
})
xml.endElement()
function test(options, callback) {
installer.installTest(options, (ip, success) => {
if (success) properties.runUnitTestsScript ? runTestScript(properties.runUnitTestsScript, [ip]) : log.error(`runUnitTestScript missing from properties`)
})
xml.endElement()
xml.endDocument()
const testOut = `./${properties.sourceDir}/tests`
mkdirp(testOut)
log.info(`writing ${testOut}/ukorTests.xml and .junit`)
fs.writeFileSync(path.join(testOut, 'ukorTests.junit'), xml.toString())
fs.writeFileSync(path.join(testOut, 'ukorTests.xml'), xml.toString())
log.info('successfully wrote junit xml')
}

function runTests(ip, auth, port, callback) {
setTimeout(() => {
let url =
'http://' +
ip +
':8060/launch/dev?RunTests=true&host=' +
getIP.address() +
'&port=' +
port
log.debug('starting tests with: %s', url)
request.post(
url,
{
auth: auth
},
(err, response, body) => {
if (response.statusCode != 200) {
log.error('Error starting tests: %d', response.statusCode)
process.exit(-1)
} else {
log.info('Tests started')
}
callback ? callback(response.statusCode == 200) : null
}
)
}, 10*1000) //run tests 10 seconds after pushing
}

module.exports = {
test: (options, callback) => {
installer.installTest(options, (ip, success) => {
log.info('finished install, running tests')
if (!success) return
let port = options.port || '8086'
let server = runLogServer(ip, port, 5 * 60 * 1000, results => {
if (results) {
process.exit(0)
} else {
process.exit(-1)
}
})
runTests(ip, options.auth, options.port, success => {
if (!success) {
server.close()
process.exit(-1)
}
})
})
}
test
}
4 changes: 2 additions & 2 deletions lib/sample/src/main/components/AppScene.brs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sub init()

end sub
' your code here
end sub
12 changes: 3 additions & 9 deletions lib/sample/src/main/components/AppScene.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>

<component name="AppScene" extends="Scene" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">
<interface>
</interface>
<script type="text/brightscript" uri="@{components}/AppScene.brs"/>
<children>
</children>
</component>
<component name="AppScene" extends="Scene">
<script type="text/brightscript" uri="@{components}/AppScene.brs"/>
</component>
8 changes: 8 additions & 0 deletions lib/sample/src/main/manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title=UkorBaseProject
major_version=1
minor_version=0
build_version=1
mm_icon_focus_hd=
mm_icon_focus_sd=
splash_screen_hd=
splash_screen_sd=
Loading