Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'newapi'

Conflicts:
	package.json
  • Loading branch information...
commit f9336c562e23568aafbdf4b15714d86d28da5cb5 2 parents 17da69a + 23576f8
@rvagg rvagg authored
Showing with 1,299 additions and 572 deletions.
  1. +15 −0 credits.js
  2. +1 −1  credits.txt
  3. +40 −0 exercises/baby_steps/exercise.js
  4. 0  {problems → exercises}/baby_steps/problem.md
  5. 0  {problems/baby_steps → exercises/baby_steps/solution}/solution.js
  6. +92 −0 exercises/filtered_ls/exercise.js
  7. 0  {problems → exercises}/filtered_ls/file-list.json
  8. 0  {problems → exercises}/filtered_ls/problem.md
  9. 0  {problems/filtered_ls → exercises/filtered_ls/solution}/solution.js
  10. +17 −0 exercises/hello_world/exercise.js
  11. 0  {problems → exercises}/hello_world/problem.md
  12. 0  {problems/hello_world → exercises/hello_world/solution}/solution.js
  13. +56 −0 exercises/http_client/exercise.js
  14. 0  {problems → exercises}/http_client/problem.md
  15. 0  {problems/http_client → exercises/http_client/solution}/solution.js
  16. +57 −0 exercises/http_collect/exercise.js
  17. 0  {problems → exercises}/http_collect/problem.md
  18. 0  {problems/http_collect → exercises/http_collect/solution}/solution.js
  19. +114 −0 exercises/http_file_server/exercise.js
  20. 0  {problems → exercises}/http_file_server/problem.md
  21. +3 −0  {problems/http_file_server → exercises/http_file_server/solution}/solution.js
  22. +97 −0 exercises/http_json_api_server/exercise.js
  23. 0  {problems → exercises}/http_json_api_server/problem.md
  24. 0  {problems/http_json_api_server → exercises/http_json_api_server/solution}/solution.js
  25. +92 −0 exercises/http_uppercaserer/exercise.js
  26. 0  {problems → exercises}/http_uppercaserer/problem.md
  27. 0  {problems/http_uppercaserer → exercises/http_uppercaserer/solution}/solution.js
  28. +93 −0 exercises/juggling_async/exercise.js
  29. 0  {problems → exercises}/juggling_async/problem.md
  30. 0  {problems/juggling_async → exercises/juggling_async/solution}/solution.js
  31. +82 −0 exercises/make_it_modular/exercise.js
  32. 0  {problems → exercises}/make_it_modular/problem.md
  33. 0  {problems/make_it_modular → exercises/make_it_modular/solution}/solution.js
  34. 0  {problems/make_it_modular → exercises/make_it_modular/solution}/solution_filter.js
  35. +218 −0 exercises/make_it_modular/verify.js
  36. +5 −0 exercises/make_it_modular/wrap-requires.js
  37. 0  { → exercises}/menu.json
  38. +78 −0 exercises/my_first_async_io/exercise.js
  39. 0  {problems → exercises}/my_first_async_io/problem.md
  40. 0  {problems/my_first_async_io → exercises/my_first_async_io/solution}/solution.js
  41. +78 −0 exercises/my_first_io/exercise.js
  42. 0  {problems → exercises}/my_first_io/problem.md
  43. 0  {problems/my_first_io → exercises/my_first_io/solution}/solution.js
  44. +36 −0 exercises/my_first_io/wrap.js
  45. +80 −0 exercises/time_server/exercise.js
  46. 0  {problems → exercises}/time_server/problem.md
  47. 0  {problems → exercises}/time_server/setup.js
  48. +1 −0  {problems/time_server → exercises/time_server/solution}/solution.js
  49. +24 −7 learnyounode.js
  50. +3 −0  lib/rndport.js
  51. +17 −9 package.json
  52. +0 −11 problems/baby_steps/setup.js
  53. +0 −25 problems/filtered_ls/setup.js
  54. +0 −3  problems/hello_world/setup.js
  55. +0 −18 problems/http_client/setup.js
  56. +0 −16 problems/http_collect/setup.js
  57. +0 −70 problems/http_file_server/setup.js
  58. +0 −55 problems/http_json_api_server/setup.js
  59. +0 −53 problems/http_uppercaserer/setup.js
  60. +0 −43 problems/juggling_async/setup.js
  61. +0 −29 problems/make_it_modular/setup.js
  62. +0 −184 problems/make_it_modular/verify.js
  63. +0 −24 problems/my_first_async_io/setup.js
  64. +0 −24 problems/my_first_io/setup.js
View
15 credits.js
@@ -0,0 +1,15 @@
+const fs = require('fs')
+ , path = require('path')
+ , colorsTmpl = require('colors-tmpl')
+
+
+function credits () {
+ fs.readFile(path.join(__dirname, './credits.txt'), 'utf8', function (err, data) {
+ if (err)
+ throw err
+
+ console.log(colorsTmpl(data))
+ })
+}
+
+module.exports = credits
View
2  credits.txt
@@ -1,4 +1,4 @@
-{yellow}{bold}{appname} is brought to you by the following dedicated hackers:{/bold}{/yellow}
+{yellow}{bold}learnyounode is brought to you by the following dedicated hackers:{/bold}{/yellow}
{bold}Name GitHub Username{/bold}
-----------------------------------
View
40 exercises/baby_steps/exercise.js
@@ -0,0 +1,40 @@
+var exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// generate a random positive integer <= 100
+
+function rndint () {
+ return Math.ceil(Math.random() * 100)
+}
+
+
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ // create a random batch of cmdline args
+ var args = [ rndint(), rndint() ]
+
+ while (Math.random() > 0.3)
+ args.push(rndint())
+
+ // supply the args to the 'execute' processor for both
+ // solution and submission spawn()
+ this.submissionArgs = this.solutionArgs = args
+
+ process.nextTick(callback)
+})
+
+module.exports = exercise
View
0  problems/baby_steps/problem.md → exercises/baby_steps/problem.md
File renamed without changes
View
0  problems/baby_steps/solution.js → exercises/baby_steps/solution/solution.js
File renamed without changes
View
92 exercises/filtered_ls/exercise.js
@@ -0,0 +1,92 @@
+var fs = require('fs')
+ , path = require('path')
+ , os = require('os')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , wrappedexec = require('workshopper-wrappedexec')
+ , after = require('after')
+ , rimraf = require('rimraf')
+ , files = require('./file-list')
+
+ , testDir = path.join(os.tmpDir(), '_learnyounode_' + process.pid)
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+// wrap up the child process in a phantom wrapper that can
+// mess with the global environment and inspect execution
+exercise = wrappedexec(exercise)
+
+// a module we want run just prior to the submission in the
+// child process
+exercise.wrapModule(require.resolve('../my_first_io/wrap'))
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ // supply the dir and extensions as args to the 'execute' processor for both
+ // solution and submission spawn()
+ // using unshift here because wrappedexec needs to use additional
+ // args to do its magic
+ this.submissionArgs.unshift('md')
+ this.submissionArgs.unshift(testDir)
+ this.solutionArgs.unshift('md')
+ this.solutionArgs.unshift(testDir)
+
+ fs.mkdir(testDir, function (err) {
+ if (err)
+ return callback(err)
+
+ var done = after(files.length, callback)
+
+ files.forEach(function (f) {
+ fs.writeFile(
+ path.join(testDir, f)
+ , 'nothing to see here'
+ , 'utf8'
+ , done
+ )
+ })
+ })
+})
+
+
+// add a processor only for 'verify' calls
+exercise.addVerifyProcessor(function (callback) {
+ var usedSync = false
+ , usedAsync = false
+
+ Object.keys(exercise.wrapData.fsCalls).forEach(function (m) {
+ if (/Sync$/.test(m)) {
+ usedSync = true
+ this.emit('fail', 'Used synchronous method: fs.' + m + '()')
+ } else {
+ usedAsync = true
+ this.emit('pass', 'Used asynchronous method: fs.' + m + '()')
+ }
+ }.bind(this))
+
+ callback(null, usedAsync && !usedSync)
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ rimraf(testDir, callback)
+})
+
+
+module.exports = exercise
View
0  problems/filtered_ls/file-list.json → exercises/filtered_ls/file-list.json
File renamed without changes
View
0  problems/filtered_ls/problem.md → exercises/filtered_ls/problem.md
File renamed without changes
View
0  problems/filtered_ls/solution.js → exercises/filtered_ls/solution/solution.js
File renamed without changes
View
17 exercises/hello_world/exercise.js
@@ -0,0 +1,17 @@
+var exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+module.exports = exercise
View
0  problems/hello_world/problem.md → exercises/hello_world/problem.md
File renamed without changes
View
0  problems/hello_world/solution.js → exercises/hello_world/solution/solution.js
File renamed without changes
View
56 exercises/http_client/exercise.js
@@ -0,0 +1,56 @@
+var http = require('http')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+
+ , words = require('boganipsum/clean_words')
+ .sort(function () { return 0.5 - Math.random() })
+ .slice(0, 10)
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ this.server = http.createServer(function (req, res) {
+ // use setTimeout to slow down the output to test timing
+ ;(function next (i) {
+ if (i == words.length)
+ return res.end()
+ res.write(words[i].trim())
+ setTimeout(next.bind(null, i + 1), 10)
+ }(0))
+ })
+
+ this.server.listen(0, function () {
+ var url = 'http://localhost:' + String(this.server.address().port)
+
+ // give the url as the first cmdline arg to the child processes
+ this.submissionArgs = [ url ]
+ this.solutionArgs = [ url ]
+
+ callback()
+ }.bind(this))
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ this.server.close(callback)
+})
+
+
+module.exports = exercise
View
0  problems/http_client/problem.md → exercises/http_client/problem.md
File renamed without changes
View
0  problems/http_client/solution.js → exercises/http_client/solution/solution.js
File renamed without changes
View
57 exercises/http_collect/exercise.js
@@ -0,0 +1,57 @@
+var http = require('http')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+
+ , words = require('boganipsum')({ paragraphs: 2, sentenceMax: 1 }).split(' ')
+
+
+// the output will be long lines so make the comparison take that into account
+exercise.longCompareOutput = true
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ this.server = http.createServer(function (req, res) {
+ // use setTimeout to slow down the output to test timing
+ ;(function next (i) {
+ if (i == words.length)
+ return res.end()
+ res.write(words[i] + ' ')
+ setTimeout(next.bind(null, i + 1), 2)
+ }(0))
+ })
+
+ this.server.listen(0, function () {
+ var url = 'http://localhost:' + String(this.server.address().port)
+
+ // give the url as the first cmdline arg to the child processes
+ this.submissionArgs = [ url ]
+ this.solutionArgs = [ url ]
+
+ callback()
+ }.bind(this))
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ this.server.close(callback)
+})
+
+
+module.exports = exercise
View
0  problems/http_collect/problem.md → exercises/http_collect/problem.md
File renamed without changes
View
0  problems/http_collect/solution.js → exercises/http_collect/solution/solution.js
File renamed without changes
View
114 exercises/http_file_server/exercise.js
@@ -0,0 +1,114 @@
+var fs = require('fs')
+ , path = require('path')
+ , os = require('os')
+ , through2 = require('through2')
+ , hyperquest = require('hyperquest')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , wrappedexec = require('workshopper-wrappedexec')
+ , rndtxt = require('boganipsum')({ paragraphs: 1, sentenceMax: 1 }) + '\n'
+ , testFile = path.join(os.tmpDir(), '_learnyounode_' + process.pid + '.txt')
+ , rndport = require('../../lib/rndport')
+
+
+// the output will be long lines so make the comparison take that into account
+exercise.longCompareOutput = true
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// add a processor for both run and verify calls, added *before*
+// the comparestdout processor so we can mess with the stdouts
+exercise.addProcessor(function (mode, callback) {
+ this.submissionStdout.pipe(process.stdout)
+
+ // replace stdout with our own streams
+ this.submissionStdout = through2()
+ if (mode == 'verify')
+ this.solutionStdout = through2()
+
+ setTimeout(query.bind(this, mode), 500)
+
+ process.nextTick(function () {
+ callback(null, true)
+ })
+})
+
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// wrap up the child process in a phantom wrapper that can
+// mess with the global environment and inspect execution
+exercise = wrappedexec(exercise)
+
+// a module we want run just prior to the submission in the
+// child process
+exercise.wrapModule(require.resolve('../my_first_io/wrap'))
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ this.submissionPort = rndport()
+ this.solutionPort = this.submissionPort + 1
+
+ this.submissionArgs.unshift(testFile)
+ this.submissionArgs.unshift(this.submissionPort)
+ this.solutionArgs.unshift(testFile)
+ this.solutionArgs.unshift(this.solutionPort)
+
+ fs.writeFile(testFile, rndtxt, 'utf8', callback)
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ fs.unlink(testFile, callback)
+})
+
+
+// delayed for 500ms to wait for servers to start so we can start
+// playing with them
+function query (mode) {
+ var exercise = this
+
+ function connect (port, stream) {
+ //TODO: introduce verification of content-type:text/plain and statusCode=200
+ hyperquest.get('http://localhost:' + port)
+ .on('error', function (err) {
+ exercise.emit(
+ 'fail'
+ , 'Error connecting to http://localhost:' + port + ': ' + err.message
+ )
+ })
+ .pipe(stream)
+ }
+
+ connect(this.submissionPort, this.submissionStdout)
+
+ if (mode == 'verify')
+ connect(this.solutionPort, this.solutionStdout)
+}
+
+
+// add a processor only for 'verify' calls
+exercise.addVerifyProcessor(function (callback) {
+ var exercise = this
+ , badCalls = Object.keys(exercise.wrapData.fsCalls).filter(function (m) {
+ exercise.emit('fail', 'Used fs method other than fs.createReadStream(): fs.' + m + '()')
+ return !(/createReadStream/).test(m)
+ })
+
+ callback(null, badCalls.length === 0)
+})
+
+
+module.exports = exercise
View
0  problems/http_file_server/problem.md → exercises/http_file_server/problem.md
File renamed without changes
View
3  problems/http_file_server/solution.js → ...ses/http_file_server/solution/solution.js
@@ -2,6 +2,9 @@ var http = require('http')
var fs = require('fs')
var server = http.createServer(function (req, res) {
+ res.writeHead(200, { 'content-type': 'text/plain' })
+
fs.createReadStream(process.argv[3]).pipe(res)
})
+
server.listen(Number(process.argv[2]))
View
97 exercises/http_json_api_server/exercise.js
@@ -0,0 +1,97 @@
+var through2 = require('through2')
+ , hyperquest = require('hyperquest')
+ , bl = require('bl')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , rndport = require('../../lib/rndport')
+
+ , date = new Date(Date.now() - 100000)
+
+
+// the output will be long lines so make the comparison take that into account
+exercise.longCompareOutput = true
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ this.submissionPort = rndport()
+ this.solutionPort = this.submissionPort + 1
+
+ this.submissionArgs = [ this.submissionPort ]
+ this.solutionArgs = [ this.solutionPort ]
+
+ process.nextTick(callback)
+})
+
+
+// add a processor for both run and verify calls, added *before*
+// the comparestdout processor so we can mess with the stdouts
+exercise.addProcessor(function (mode, callback) {
+ this.submissionStdout.pipe(process.stdout)
+
+ // replace stdout with our own streams
+ this.submissionStdout = through2()
+ if (mode == 'verify')
+ this.solutionStdout = through2()
+
+ setTimeout(query.bind(this, mode), 500)
+
+ process.nextTick(function () {
+ callback(null, true)
+ })
+})
+
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// delayed for 500ms to wait for servers to start so we can start
+// playing with them
+function query (mode) {
+ var exercise = this
+
+ function verify (port, stream) {
+ function error (port, err) {
+ exercise.emit(
+ 'fail'
+ , 'Error connecting to http://localhost:' + port + ': ' + err.message
+ )
+ }
+
+ hyperquest.get('http://localhost:' + port + '/api/parsetime?iso=' + date.toISOString())
+ .on('error', error)
+ .pipe(bl(function (err, data) {
+ if (err)
+ return stream.emit('error', err)
+
+ stream.write(data.toString() + '\n')
+
+ hyperquest.get('http://localhost:' + port + '/api/unixtime?iso=' + date.toISOString())
+ .on('error', error)
+ .pipe(bl(function (err, data) {
+ if (err)
+ return stream.emit('error', err)
+
+ stream.write(data.toString() + '\n')
+ stream.end()
+ }))
+ }))
+ }
+
+ verify(this.submissionPort, this.submissionStdout)
+
+ if (mode == 'verify')
+ verify(this.solutionPort, this.solutionStdout)
+}
+
+
+module.exports = exercise
View
0  problems/http_json_api_server/problem.md → exercises/http_json_api_server/problem.md
File renamed without changes
View
0  problems/http_json_api_server/solution.js → ...http_json_api_server/solution/solution.js
File renamed without changes
View
92 exercises/http_uppercaserer/exercise.js
@@ -0,0 +1,92 @@
+var through2 = require('through2')
+ , hyperquest = require('hyperquest')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , rndport = require('../../lib/rndport')
+ , words = require('boganipsum/clean_words')
+ .sort(function () { return 0.5 - Math.random() })
+ .slice(0, 10)
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ this.submissionPort = rndport()
+ this.solutionPort = this.submissionPort + 1
+
+ this.submissionArgs = [ this.submissionPort ]
+ this.solutionArgs = [ this.solutionPort ]
+
+ process.nextTick(callback)
+})
+
+
+// add a processor for both run and verify calls, added *before*
+// the comparestdout processor so we can mess with the stdouts
+exercise.addProcessor(function (mode, callback) {
+ this.submissionStdout.pipe(process.stdout)
+
+ // replace stdout with our own streams
+ this.submissionStdout = through2()
+ if (mode == 'verify')
+ this.solutionStdout = through2()
+
+ setTimeout(query.bind(this, mode), 500)
+
+ process.nextTick(function () {
+ callback(null, true)
+ })
+})
+
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// delayed for 500ms to wait for servers to start so we can start
+// playing with them
+function query (mode) {
+ var exercise = this
+
+ function connect (port, stream) {
+ var input = through2()
+ , count = 0
+ , iv
+
+ //TODO: test GET requests for #fail
+ input.pipe(hyperquest.post('http://localhost:' + port)
+ .on('error', function (err) {
+ exercise.emit(
+ 'fail'
+ , 'Error connecting to http://localhost:' + port + ': ' + err.message
+ )
+ }))
+ .pipe(stream)
+
+ iv = setInterval(function () {
+ input.write(words[count].trim() + '\n')
+
+ if (++count == words.length) {
+ clearInterval(iv)
+ input.end()
+ }
+ }, 50)
+
+ }
+
+ connect(this.submissionPort, this.submissionStdout)
+
+ if (mode == 'verify')
+ connect(this.solutionPort, this.solutionStdout)
+}
+
+
+module.exports = exercise
View
0  problems/http_uppercaserer/problem.md → exercises/http_uppercaserer/problem.md
File renamed without changes
View
0  problems/http_uppercaserer/solution.js → ...es/http_uppercaserer/solution/solution.js
File renamed without changes
View
93 exercises/juggling_async/exercise.js
@@ -0,0 +1,93 @@
+var http = require('http')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , bogan = require('boganipsum')
+ , after = require('after')
+
+ // three separate chunks of words to spit out
+ , words = [
+ bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
+ , bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
+ , bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
+ ]
+
+
+// the output will be long lines so make the comparison take that into account
+exercise.longCompareOutput = true
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// write the words out to the client for this server, do it slowly
+// and wait for `delay` until we start to make async handling a pain
+function writeWords(i, delay, res) {
+ setTimeout(function () {
+ ;(function next (j) {
+ if (j == words[i].length)
+ return res.end()
+ res.write(words[i][j] + ' ')
+ // use setTimeout to slow down the output to test timing
+ setTimeout(next.bind(null, j + 1), 2)
+ }(0))
+ }, delay)
+}
+
+
+// start a server to print `words[i]` after `delay`
+function server (i, delay, callback) {
+ return http.createServer(function (req, res) {
+ writeWords(i, delay, res)
+ }).listen(0, callback)
+}
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ var done = after(3, function (err) {
+ if (err)
+ return callback(err)
+
+ // give the 3 server urls as cmdline args to the child processes
+ var args = this.servers.map(function (s) {
+ return 'http://localhost:' + s.address().port
+ })
+
+ this.submissionArgs = args
+ this.solutionArgs = args
+
+ callback()
+ }.bind(this))
+
+ this.servers = [
+ server(0, 200, done)
+ , server(1, 0, done)
+ , server(2, 100, done)
+ ]
+
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ // close all 3 servers
+ var done = after(3, callback)
+ this.servers.forEach(function (s) {
+ s.close(done)
+ })
+})
+
+
+module.exports = exercise
View
0  problems/juggling_async/problem.md → exercises/juggling_async/problem.md
File renamed without changes
View
0  problems/juggling_async/solution.js → ...cises/juggling_async/solution/solution.js
File renamed without changes
View
82 exercises/make_it_modular/exercise.js
@@ -0,0 +1,82 @@
+var fs = require('fs')
+ , path = require('path')
+ , os = require('os')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , wrappedexec = require('workshopper-wrappedexec')
+ , after = require('after')
+ , rimraf = require('rimraf')
+ , verify = require('./verify')
+ , files = require('../filtered_ls/file-list')
+
+ , testDir = path.join(os.tmpDir(), '_learnyounode_' + process.pid)
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+// wrap up the child process in a phantom wrapper that can
+// mess with the global environment and inspect execution
+exercise = wrappedexec(exercise)
+
+// modules we want run just prior to the submission in the
+// child process
+exercise.wrapModule(require.resolve('../my_first_io/wrap'))
+exercise.wrapModule(require.resolve('./wrap-requires'))
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ // store for later use by verify()
+ this._testDir = testDir
+
+ // supply the dir and extensions as args to the 'execute' processor for both
+ // solution and submission spawn()
+ // using unshift here because wrappedexec needs to use additional
+ // args to do its magic
+ this.submissionArgs.unshift('md')
+ this.submissionArgs.unshift(testDir)
+ this.solutionArgs.unshift('md')
+ this.solutionArgs.unshift(testDir)
+
+ fs.mkdir(testDir, function (err) {
+ if (err)
+ return callback(err)
+
+ var done = after(files.length, callback)
+
+ files.forEach(function (f) {
+ fs.writeFile(
+ path.join(testDir, f)
+ , 'nothing to see here'
+ , 'utf8'
+ , done
+ )
+ })
+ })
+})
+
+
+// add a processor only for 'verify' calls
+exercise.addVerifyProcessor(verify)
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ rimraf(testDir, callback)
+})
+
+
+module.exports = exercise
View
0  problems/make_it_modular/problem.md → exercises/make_it_modular/problem.md
File renamed without changes
View
0  problems/make_it_modular/solution.js → ...ises/make_it_modular/solution/solution.js
File renamed without changes
View
0  problems/make_it_modular/solution_filter.js → ...ke_it_modular/solution/solution_filter.js
File renamed without changes
View
218 exercises/make_it_modular/verify.js
@@ -0,0 +1,218 @@
+const fs = require('fs')
+ , path = require('path')
+ , util = require('util')
+ , files = require('../filtered_ls/file-list')
+ , chalk = require('chalk')
+
+function validateModule (modFile, callback) {
+ var exercise = this
+ , dir = this._testDir
+ , mod
+ , error = new Error('testing')
+ , returned = false
+ , _callback = callback
+ , callbackUsed
+
+ try {
+ mod = require(modFile)
+ } catch (e) {
+ exercise.emit('fail', 'Error loading module: ' + e.message)
+ return callback(null, false)
+ }
+
+ callback = function () {
+ returned = true
+ _callback.apply(this, arguments)
+ }
+
+ function modFileError (txt) {
+ exercise.emit('fail', 'Your additional module file [' + path.basename(modFile) + '] ' + txt)
+ callback (null, false)
+ }
+
+ //---- Check that our module file is `module.exports = function () {}`
+
+ if (typeof mod != 'function') {
+ return modFileError(
+ 'does not export a ' + chalk.bold('single function') + '.'
+ + 'You must use the `module.exports = function () {}` pattern.'
+ )
+ } else {
+ exercise.emit('pass', 'Additional module file exports a single function')
+ }
+
+ //---- Check that the function exported takes 3 arguments
+
+ if (mod.length < 3) {
+ return modFileError(
+ 'exports a function that takes fewer than ' + chalk.bold('three') + ' arguments.'
+ + 'You must accept a directory, a filter and a ' + chalk.bold('callback') + '.'
+ )
+ } else {
+ exercise.emit('pass', 'Additional module file exports a function that takes ' + mod.length + ' arguments')
+ }
+
+ //---- Mock `fs.readdir` and check that an error bubbles back up through the cb
+
+ fs.$readdir = fs.readdir
+ fs.readdir = function (dir, callback) {
+ callback(error)
+ }
+
+ function noerr () {
+ modFileError(
+ 'does not appear to pass back an error received from `fs.readdir()`'
+ + 'Use the following idiomatic Node.js pattern inside your callback to `fs.readdir()`:'
+ + '\n\tif (err)\n\t return callback(err)'
+ )
+ }
+
+ callbackUsed = false
+ try {
+ mod('/foo/bar/', 'wheee', function (err) {
+ if (err !== error)
+ return noerr()
+
+ callbackUsed = true
+ })
+ } catch (e) {
+ noerr()
+ }
+
+ if (callbackUsed)
+ exercise.emit('pass', 'Additional module file handles errors properly')
+
+ //---- Check whether the callback is used at all
+
+ setTimeout(function () {
+ if (returned)
+ return
+
+ if (!callbackUsed)
+ return modFileError('did not call the callback argument after an error from fs.readdir()')
+
+ exercise.emit('pass', 'Additional module file handles callback argument')
+
+ // replace the mock readdir
+ fs.readdir = fs.$readdir
+
+ callbackUsed = false
+ try {
+ mod(dir, 'md', function (err, list) {
+ if (err) {
+ return modFileError(
+ 'returned an error on its callback:'
+ + '\n\t' + util.inspect(err)
+ )
+ }
+
+ //---- Check that we got the correct number of elements
+ if (arguments.length < 2) {
+ return modFileError(
+ 'did not return two arguments on the callback function (expected `null` and an Array of filenames)'
+ )
+ }
+
+ exercise.emit('pass', 'Additional module file returned two arguments on the callback function')
+
+ //---- Check that we got an Array as the second argument
+ if (!Array.isArray(list)) {
+ return modFileError(
+ 'did not return an Array object as the second argument of the callback'
+ )
+ }
+
+ exercise.emit('pass', 'Additional module file returned Array as second argument of the callback')
+
+ //---- Check that we got the expected number of elements in the Array
+ var exp = files.filter(function (f) { return (/\.md$/).test(f) })
+ , i
+
+ if (exp.length !== list.length) {
+ return modFileError(
+ 'did not return an Array with the correct number of elements as the second argument of the callback'
+ )
+ }
+
+ exercise.emit('pass', 'Additional module file returned correct number of elements as the second argument of the callback')
+
+ callbackUsed = true
+
+ //---- Check that the elements are exactly the same as expected (ignoring order)
+ exp.sort()
+ list.sort()
+ for (i = 0; i < exp.length; i++) {
+ if (list[i] !== exp[i]) {
+ return modFileError(
+ 'did not return the correct list of files as the second argument of the callback'
+ )
+ }
+ }
+
+ exercise.emit('pass', 'Additional module file returned correct list of files as the second argument of the callback')
+
+ //WIN!!
+ callback()
+ })
+ } catch (e) {
+ return modFileError(
+ 'threw an error:'
+ + '\n\t' + util.inspect(e)
+ )
+ }
+
+ setTimeout(function () {
+ if (returned)
+ return
+
+ if (!callbackUsed)
+ return modFileError('did not call the callback argument')
+ }, 300)
+ }, 300)
+}
+
+
+// find any modules that are required by the submission program
+
+function requires (exercise) {
+ // rule out these 4 things
+ var main = path.resolve(process.cwd(), exercise.args[0])
+ , exec = require.resolve('workshopper-wrappedexec/exec-wrap')
+ , wrap1 = require.resolve('../my_first_io/wrap')
+ , wrap2 = require.resolve('./wrap-requires')
+
+ return exercise.wrapData.requires.filter(function (m) {
+ return m != main && m != exec && m != wrap1 && m != wrap2
+ })
+}
+
+
+function verifyModuleUsed (callback) {
+ var required = requires(this)
+
+ if (required.length === 0) {
+ this.emit('fail', 'Did not use an additional module file, you must require() a module to help solve this exercise')
+ return callback(null, false)
+ }
+
+ validateModule.call(this, required[0], callback)
+}
+
+function verify (callback) {
+ var usedSync = false
+ , usedAsync = false
+
+ Object.keys(this.wrapData.fsCalls).forEach(function (m) {
+ if (/Sync$/.test(m)) {
+ usedSync = true
+ this.emit('fail', 'Used synchronous method: fs.' + m + '()')
+ } else {
+ usedAsync = true
+ this.emit('pass', 'Used asynchronous method: fs.' + m + '()')
+ }
+ }.bind(this))
+
+ verifyModuleUsed.call(this, callback)
+}
+
+module.exports = verify
View
5 exercises/make_it_modular/wrap-requires.js
@@ -0,0 +1,5 @@
+function finish (ctx) {
+ ctx.requires = Object.keys(require.cache)
+}
+
+module.exports.finish = finish
View
0  menu.json → exercises/menu.json
File renamed without changes
View
78 exercises/my_first_async_io/exercise.js
@@ -0,0 +1,78 @@
+var fs = require('fs')
+ , path = require('path')
+ , os = require('os')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , wrappedexec = require('workshopper-wrappedexec')
+ , boganipsum = require('boganipsum')
+
+ , testFile = path.join(os.tmpDir(), '_learnyounode_' + process.pid + '.txt')
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+// wrap up the child process in a phantom wrapper that can
+// mess with the global environment and inspect execution
+exercise = wrappedexec(exercise)
+
+// a module we want run just prior to the submission in the
+// child process
+exercise.wrapModule(require.resolve('../my_first_io/wrap'))
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ var lines = Math.ceil(Math.random() * 50)
+ , txt = boganipsum({ paragraphs: lines })
+
+ // supply the file as an arg to the 'execute' processor for both
+ // solution and submission spawn()
+ // using unshift here because wrappedexec needs to use additional
+ // args to do its magic
+ this.submissionArgs.unshift(testFile)
+ this.solutionArgs.unshift(testFile)
+
+ // file with random text
+ fs.writeFile(testFile, txt, 'utf8', callback)
+})
+
+
+// add a processor only for 'verify' calls
+exercise.addVerifyProcessor(function (callback) {
+ var usedSync = false
+ , usedAsync = false
+
+ Object.keys(exercise.wrapData.fsCalls).forEach(function (m) {
+ if (/Sync$/.test(m)) {
+ usedSync = true
+ this.emit('fail', 'Used synchronous method: fs.' + m + '()')
+ } else {
+ usedAsync = true
+ this.emit('pass', 'Used asynchronous method: fs.' + m + '()')
+ }
+ }.bind(this))
+
+ callback(null, usedAsync && !usedSync)
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ fs.unlink(testFile, callback)
+})
+
+
+module.exports = exercise
View
0  problems/my_first_async_io/problem.md → exercises/my_first_async_io/problem.md
File renamed without changes
View
0  problems/my_first_async_io/solution.js → ...es/my_first_async_io/solution/solution.js
File renamed without changes
View
78 exercises/my_first_io/exercise.js
@@ -0,0 +1,78 @@
+var fs = require('fs')
+ , path = require('path')
+ , os = require('os')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , wrappedexec = require('workshopper-wrappedexec')
+ , boganipsum = require('boganipsum')
+
+ , testFile = path.join(os.tmpDir(), '_learnyounode_' + process.pid + '.txt')
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+// wrap up the child process in a phantom wrapper that can
+// mess with the global environment and inspect execution
+exercise = wrappedexec(exercise)
+
+// a module we want run just prior to the submission in the
+// child process
+exercise.wrapModule(require.resolve('./wrap'))
+
+
+// set up the data file to be passed to the submission
+exercise.addSetup(function (mode, callback) {
+ // mode == 'run' || 'verify'
+
+ var lines = Math.ceil(Math.random() * 50)
+ , txt = boganipsum({ paragraphs: lines })
+
+ // supply the file as an arg to the 'execute' processor for both
+ // solution and submission spawn()
+ // using unshift here because wrappedexec needs to use additional
+ // args to do its magic
+ this.submissionArgs.unshift(testFile)
+ this.solutionArgs.unshift(testFile)
+
+ // file with random text
+ fs.writeFile(testFile, txt, 'utf8', callback)
+})
+
+
+// add a processor only for 'verify' calls
+exercise.addVerifyProcessor(function (callback) {
+ var usedSync = false
+ , usedAsync = false
+
+ Object.keys(exercise.wrapData.fsCalls).forEach(function (m) {
+ if (/Sync$/.test(m)) {
+ usedSync = true
+ this.emit('pass', 'Used synchronous method: fs.' + m + '()')
+ } else {
+ usedAsync = true
+ this.emit('fail', 'Used asynchronous method: fs.' + m + '()')
+ }
+ }.bind(this))
+
+ callback(null, !usedAsync && usedSync)
+})
+
+
+// cleanup for both run and verify
+exercise.addCleanup(function (mode, passed, callback) {
+ // mode == 'run' || 'verify'
+
+ fs.unlink(testFile, callback)
+})
+
+
+module.exports = exercise
View
0  problems/my_first_io/problem.md → exercises/my_first_io/problem.md
File renamed without changes
View
0  problems/my_first_io/solution.js → exercises/my_first_io/solution/solution.js
File renamed without changes
View
36 exercises/my_first_io/wrap.js
@@ -0,0 +1,36 @@
+var fs = require('fs')
+
+
+function wrap (ctx) {
+ ctx.fsCalls = {}
+
+ // wrap app fs calls
+ Object.keys(fs).forEach(function (m) {
+ var orig = fs[m]
+
+ fs[m] = function () {
+ // $captureStack is a utility to capture a stacktrace array
+ var stack = ctx.$captureStack(fs[m])
+
+ // inspect the first callsite of the stacktrace and see if the
+ // filename matches the mainProgram we're running, if so, then
+ // the user has used the method in question
+ // the substring() is necessary as the user doesn't have to provide
+ // a .js extension to make it work
+
+ if (stack[0].getFileName().substring(0, ctx.mainProgram.length) == ctx.mainProgram) {
+ if (!ctx.fsCalls[m])
+ ctx.fsCalls[m] = 1
+ else
+ ctx.fsCalls[m]++
+ }
+
+ // call the real fs.readFileSync
+
+ return orig.apply(this, arguments)
+ }
+ })
+}
+
+
+module.exports = wrap
View
80 exercises/time_server/exercise.js
@@ -0,0 +1,80 @@
+var net = require('net')
+ , exercise = require('workshopper-exercise')()
+ , filecheck = require('workshopper-exercise/filecheck')
+ , execute = require('workshopper-exercise/execute')
+ , comparestdout = require('workshopper-exercise/comparestdout')
+ , through2 = require('through2')
+ , rndport = require('../../lib/rndport')
+
+
+// checks that the submission file actually exists
+exercise = filecheck(exercise)
+
+// execute the solution and submission in parallel with spawn()
+exercise = execute(exercise)
+
+
+// assign ports for the child processes to listen to
+exercise.addSetup(function (mode, callback) {
+ this.submissionPort = rndport()
+ this.solutionPort = this.submissionPort + 1
+
+ // set child process arguments
+ this.submissionArgs = [ this.submissionPort ]
+ this.solutionArgs = [ this.solutionPort ]
+
+ process.nextTick(callback)
+})
+
+
+// add a processor for both run and verify calls, added *before*
+// the comparestdout processor so we can mess with the stdouts
+exercise.addProcessor(function (mode, callback) {
+ this.submissionStdout.pipe(process.stdout)
+
+ // replace stdout with our own streams
+ this.submissionStdout = through2()
+ if (mode == 'verify')
+ this.solutionStdout = through2()
+
+ setTimeout(query.bind(this, mode), 500)
+
+ process.nextTick(function () {
+ callback(null, true)
+ })
+})
+
+// compare stdout of solution and submission
+exercise = comparestdout(exercise)
+
+
+// delayed for 500ms to wait for servers to start so we can start
+// playing with them
+function query (mode) {
+ var exercise = this
+
+ // on error, write to the stream so that'll also be verified
+
+ // connect to localhost:<port> and pipe results to <stream>
+ function connect (port, stream) {
+ net.connect(port)
+ .on('error', function (err) {
+ stream.end()
+ setImmediate(function () {
+ exercise.emit(
+ 'fail'
+ , 'Error connecting to localhost:' + port + ': ' + err.message
+ )
+ })
+ })
+ .pipe(stream)
+ }
+
+ connect(this.submissionPort, this.submissionStdout)
+
+ if (mode == 'verify')
+ connect(this.solutionPort, this.solutionStdout)
+}
+
+
+module.exports = exercise
View
0  problems/time_server/problem.md → exercises/time_server/problem.md
File renamed without changes
View
0  problems/time_server/setup.js → exercises/time_server/setup.js
File renamed without changes
View
1  problems/time_server/solution.js → exercises/time_server/solution/solution.js
@@ -16,4 +16,5 @@ function now () {
var server = net.createServer(function (socket) {
socket.end(now() + '\n')
})
+
server.listen(Number(process.argv[2]))
View
31 learnyounode.js
@@ -1,12 +1,29 @@
#!/usr/bin/env node
-const Workshopper = require('workshopper')
+const workshopper = require('workshopper')
, path = require('path')
+ , credits = require('./credits')
+ , menu = require('./exercises/menu')
-Workshopper({
- name : 'learnyounode'
- , title : 'LEARN YOU THE NODE.JS FOR MUCH WIN!'
+ , name = 'learnyounode'
+ , title = 'LEARN YOU THE NODE.JS FOR MUCH WIN!'
+ , subtitle = '\x1b[23mSelect an exercise and hit \x1b[3mEnter\x1b[23m to begin'
+
+
+function fpath (f) {
+ return path.join(__dirname, f)
+}
+
+
+workshopper({
+ name : name
+ , title : title
+ , subtitle : subtitle
+ , exerciseDir : fpath('./exercises/')
, appDir : __dirname
- , helpFile : path.join(__dirname, 'help.txt')
- , creditsFile : path.join(__dirname, 'credits.txt')
-}).init()
+ , helpFile : fpath('help.txt')
+ , menuItems : [ {
+ name : 'credits'
+ , handler : credits
+ } ]
+})
View
3  lib/rndport.js
@@ -0,0 +1,3 @@
+module.exports = function rndport () {
+ return 1024 + Math.floor(Math.random() * 64511)
+}
View
26 package.json
@@ -1,6 +1,6 @@
{
"name": "learnyounode",
- "version": "0.4.1",
+ "version": "1.0.0-alpha01",
"description": "Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.",
"author": "Rod Vagg <rod@vagg.org> (https://github.com/rvagg)",
"contributors": [
@@ -17,14 +17,22 @@
},
"license": "MIT",
"dependencies": {
- "concat-stream": "~1.2.1",
- "duplexer": "~0.1.1",
- "through": "~2.3.4",
- "boganipsum": "~0.1.0",
- "hyperquest": "~0.1.8",
- "bl": "~0.6.0",
- "through2-map": "~1.2.0",
- "workshopper": "~0.7.0"
+ "workshopper": "^1.0.0-alpha05",
+ "workshopper-exercise": "^0.2.2",
+ "workshopper-wrappedexec": "^0.1.1",
+ "workshopper-boilerplate": "0.0.1",
+ "concat-stream": "^1.4.1",
+ "duplexer": "^0.1.1",
+ "through": "^2.3.4",
+ "boganipsum": "^0.1.0",
+ "hyperquest": "^0.2.0",
+ "bl": "^0.7.0",
+ "through2-map": "^1.2.1",
+ "colors-tmpl": "^0.1.0",
+ "after": "^0.8.1",
+ "rimraf": "^2.2.6",
+ "chalk": "^0.4.0",
+ "through2": "^0.4.1"
},
"bin": "./learnyounode.js",
"preferGlobal": true
View
11 problems/baby_steps/setup.js
@@ -1,11 +0,0 @@
-function rndint () {
- return Math.ceil(Math.random() * 100)
-}
-
-module.exports = function () {
- var args = [ rndint(), rndint() ]
- while (Math.random() > 0.3)
- args.push(rndint())
-
- return { args: args, stdin: null }
-}
View
25 problems/filtered_ls/setup.js
@@ -1,25 +0,0 @@
-const fs = require('fs')
- , path = require('path')
- , os = require('os')
- , onlyAsync = require('workshopper/verify-calls').verifyOnlyAsync
- , files = require('./file-list')
-
-module.exports = function () {
- var dir = path.join(os.tmpDir(), 'learnyounode_' + process.pid)
- , trackFile = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.json')
-
- fs.mkdirSync(dir)
- files.forEach(function (f) {
- fs.writeFileSync(path.join(dir, f), 'nothing to see here', 'utf8')
- })
-
- return {
- args : [ dir, 'dat' ]
- , stdin : null
- , modUseTrack : {
- trackFile : trackFile
- , modules : [ 'fs' ]
- }
- , verify : onlyAsync.bind(null, trackFile)
- }
-}
View
3  problems/hello_world/setup.js
@@ -1,3 +0,0 @@
-module.exports = function () {
- return { args: [], stdin: null }
-}
View
18 problems/http_client/setup.js
@@ -1,18 +0,0 @@
-const http = require('http')
- , words = require('boganipsum/clean_words')
- .sort(function () { return 0.5 - Math.random() })
- .slice(0, 10)
-
-
-module.exports = function () {
- var server = http.createServer(function (req, res) {
- words.forEach(function (w) { res.write(w.trim()) })
- res.end()
- }).listen(9345)
-
- return {
- args : [ 'http://localhost:9345' ]
- , stdin : null
- , close : server.close.bind(server)
- }
-}
View
16 problems/http_collect/setup.js
@@ -1,16 +0,0 @@
-const http = require('http')
- , words = require('boganipsum')({ paragraphs: 1, sentenceMax: 1 }).split(' ')
-
-module.exports = function () {
- var server = http.createServer(function (req, res) {
- words.forEach(function (w) { res.write(w + ' ') })
- res.end()
- }).listen(9345)
-
- return {
- args : [ 'http://localhost:9345' ]
- , stdin : null
- , long : true
- , close : server.close.bind(server)
- }
-}
View
70 problems/http_file_server/setup.js
@@ -1,70 +0,0 @@
-const through = require('through')
- , hyperquest = require('hyperquest')
- , bogan = require('boganipsum')
- , fs = require('fs')
- , path = require('path')
- , os = require('os')
- , bold = require('workshopper/term-util').bold
- , red = require('workshopper/term-util').red
-
-function verify (trackFile, callback) {
- var track = require(trackFile)
- , fscalls = track.calls.filter(function (call) {
- return call.module == 'fs'
- && call.stack[0].file != 'module.js'
- && call.stack[0].file != 'fs.js'
- })
- , badCalls = fscalls.filter(function (call) {
- return !(/createReadStream/).test(call.fn)
- })
-
- if (!badCalls.length)
- return callback() // yay!
-
- console.log('\nYou got the correct answer but used the following additional fs calls:')
- badCalls.forEach(function (call) {
- console.log('\t' + bold(red('fs.' + call.fn + '()')))
- })
- console.log('\nThis problem requires you to only use fs.createReadStream().\n')
- callback('bzzt!')
-}
-
-module.exports = function (run) {
- var file = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.txt')
- , trackFile = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.json')
- , outputA = through()
- , outputB = through()
- , portA = 1024 + Math.floor(Math.random() * 64511)
- , portB = portA+1
-
- function error (url, out, err) {
- out.write('Error connecting to ' + url + ': ' + err.message)
- out.end()
- }
-
- setTimeout(function () {
- hyperquest.get('http://localhost:' + portA)
- .on('error', error.bind(null, 'http://localhost:' + portA, outputA))
- .pipe(outputA)
- if (!run) {
- hyperquest.get('http://localhost:' + portB)
- .on('error', error.bind(null, 'http://localhost:' + portB, outputB))
- .pipe(outputB)
- }
- }, 500)
-
- fs.writeFileSync(file, bogan({ paragraphs: 1, sentenceMax: 1 }), 'utf8')
-
- return {
- submissionArgs : [portA, file]
- , solutionArgs : [portB, file]
- , a : outputA
- , b : outputB
- , modUseTrack : {
- trackFile : trackFile
- , modules : [ 'fs' ]
- }
- , verify : verify.bind(null, trackFile)
- , long : true
- }
-}
View
55 problems/http_json_api_server/setup.js
@@ -1,55 +0,0 @@
-const through = require('through')
- , hyperquest = require('hyperquest')
- , bl = require('bl')
-
- , date = new Date(Date.now() - 100000)
-
-function fetch (port) {
- var output = through()
-
- function error (err) {
- output.write('Error connecting to http://localhost:' + port + ': ' + err.message)
- output.end()
- }
-
- hyperquest.get('http://localhost:' + port + '/api/parsetime?iso=' + date.toISOString())
- .on('error', error)
- .pipe(bl(function (err, data) {
- if (err)
- return output.emit('error', err)
- output.write(data.toString() + '\n')
-
- hyperquest.get('http://localhost:' + port + '/api/unixtime?iso=' + date.toISOString())
- .on('error', error)
- .pipe(bl(function (err, data) {
- if (err)
- return output.emit('error', err)
-
- output.write(data.toString() + '\n')
- output.end()
- }))
-
- }))
- return output
-}
-
-module.exports = function (run) {
- var outputA = through()
- , outputB = through()
- , portA = 1024 + Math.floor(Math.random() * 64511)
- , portB = portA+1
-
- setTimeout(function () {
- fetch(portA).pipe(outputA)
- if (!run)
- fetch(portB).pipe(outputB)
- }, 500)
-
- return {
- submissionArgs : [portA]
- , solutionArgs : [portB]
- , a : outputA
- , b : outputB
- , long : true
- }
-}
View
53 problems/http_uppercaserer/setup.js
@@ -1,53 +0,0 @@
-const through = require('through')
- , hyperquest = require('hyperquest')
- , duplexer = require('duplexer')
- , words = require('boganipsum/clean_words')
- .sort(function () { return 0.5 - Math.random() })
- .slice(0, 10)
-
-
-module.exports = function (run) {
- var outputA = through()
- , outputB = through()
- , inputA = through().pause()
- , inputB = through().pause()
- , portA = 1024 + Math.floor(Math.random() * 64511)
- , portB = portA+1
- , count = 0
- , iv
-
- function error (url, out, err) {
- out.write('Error connecting to ' + url + ': ' + err.message)
- out.end()
- }
-
- setTimeout(function () {
- inputA.pipe(hyperquest.post('http://localhost:' + portA)
- .on('error', error.bind(null, 'http://localhost:' + portA, outputA)))
- .pipe(outputA)
- if (!run) {
- inputB.pipe(hyperquest.post('http://localhost:' + portB)
- .on('error', error.bind(null, 'http://localhost:' + portB, outputB)))
- .pipe(outputB)
- }
- }, 500)
-
- iv = setInterval(function () {
- var w = words[count].trim() + '\n'
- inputA.write(w)
- inputB.write(w)
-
- if (++count == words.length) {
- clearInterval(iv)
- inputA.end()
- inputB.end()
- }
- }, 50)
-
- return {
- submissionArgs : [portA]
- , solutionArgs : [portB]
- , a: duplexer(inputA, outputA)
- , b: duplexer(inputB, outputB)
- }
-}
View
43 problems/juggling_async/setup.js
@@ -1,43 +0,0 @@
-const http = require('http')
- , bogan = require('boganipsum')
- , words = [
- bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
- , bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
- , bogan({ paragraphs: 1, sentenceMax: 1 }).split(' ')
- ]
-
-function writeWords(i, delay, res) {
- setTimeout(function () {
- words[i].forEach(function (w) { res.write(w + ' ') })
- res.end()
- }, delay)
-}
-
-function server (i, delay, port) {
- return http.createServer(function (req, res) {
- writeWords(i, delay, res)
- }).listen(port)
-}
-
-module.exports = function () {
- var servers = [
- server(0, 100, 9345)
- , server(1, 0, 9346)
- , server(2, 50, 9347)
- ]
-
- return {
- args : [
- 'http://localhost:9345'
- , 'http://localhost:9346'
- , 'http://localhost:9347'
- ]
- , stdin : null
- , long : true
- , close : function () {
- servers.forEach(function (server) {
- server.close()
- })
- }
- }
-}
View
29 problems/make_it_modular/setup.js
@@ -1,29 +0,0 @@
-const fs = require('fs')
- , path = require('path')
- , os = require('os')
- , verify = require('./verify')
- , files = require('../filtered_ls/file-list')
-
- , dir = path.join(os.tmpDir(), 'learnyounode_' + process.pid)
- , trackFile = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.json')
-
-function setup () {
- fs.mkdirSync(dir)
- files.forEach(function (f) {
- fs.writeFileSync(path.join(dir, f), 'nothing to see here', 'utf8')
- })
-}
-
-module.exports = function () {
- setup()
-
- return {
- args : [ dir, 'md' ]
- , stdin : null
- , modUseTrack : {
- trackFile : trackFile
- , modules : [ 'fs' ]
- }
- , verify : verify.bind(null, dir, trackFile)
- }
-}
View
184 problems/make_it_modular/verify.js
@@ -1,184 +0,0 @@
-const fs = require('fs')
- , util = require('util')
- , onlyAsync = require('workshopper/verify-calls').verifyOnlyAsync
- , requires = require('workshopper/fetch-requires')
- , bold = require('workshopper/term-util').bold
- , red = require('workshopper/term-util').red
- , files = require('../filtered_ls/file-list')
-
-function validateModule (dir, trackFile, modfile, callback) {
- var mod = require(modfile)
- , error = new Error('testing')
- , returned = false
- , _callback = callback
- , callbackUsed
-
- callback = function () {
- returned = true
- _callback.apply(this, arguments)
- }
-
- function modfileError (txt) {
- console.log('\nYour additional module file:')
- console.log('\t' + modfile)
- console.log(txt + '\n')
- callback ('bzzt!')
- }
-
- //---- Check that our module file is `module.exports = function () {}`
-
- if (typeof mod != 'function') {
- return modfileError(
- 'does not export a ' + bold('single function') + '.'
- + '\n\nYou must use the `module.exports = function () {}` pattern.'
- )
- }
-
- //---- Check that the function exported takes 3 arguments
-
- if (mod.length < 3) {
- return modfileError(
- 'exports a function that takes fewer than ' + bold('three') + ' arguments.'
- + '\n\nYou must accept a directory, a filter and a ' + bold('callback') + '.'
- )
- }
-
- //---- Mock `fs.readdir` and check that an error bubbles back up through the cb
-
- fs.$readdir = fs.readdir
- fs.readdir = function (dir, callback) {
- callback(error)
- }
-
- function noerr () {
- modfileError(
- 'does not appear to pass back an error received from `fs.readdir()`'
- + '\n\nUse the following idiomatic Node.js pattern inside your callback to `fs.readdir()`:'
- + '\n\tif (err)\n\t return callback(err)'
- )
- }
-
- callbackUsed = false
- try {
- mod('/foo/bar/', 'wheee', function (err) {
- if (err !== error)
- return noerr()
-
- callbackUsed = true
- })
- } catch (e) {
- noerr()
- }
-
- //---- Check whether the callback is used at all
-
- setTimeout(function () {
- if (returned)
- return
-
- if (!callbackUsed)
- return modfileError('did not call the callback argument.')
-
- // replace the mock readdir
- fs.readdir = fs.$readdir
-
- callbackUsed = false
- try {
- mod(dir, 'md', function (err, list) {
- if (err) {
- return modfileError(
- 'returned an error on its callback:'
- + '\n\t' + util.inspect(err)
- )
- }
-
- //---- Check that we got the correct number of elements
- if (arguments.length < 2) {
- return modfileError(
- 'did not return two arguments on the callback function (expected `null` and an Array of filenames)'
- )
- }
-
- //---- Check that we got an Array as the second argument
- if (!Array.isArray(list)) {
- return modfileError(
- 'did not return an Array object as the second argument of the callback'
- )
- }
-
- //---- Check that we got the expected number of elements in the Array
- var exp = files.filter(function (f) { return (/\.md$/).test(f) })
- , i
-
- if (exp.length !== list.length) {
- return modfileError(
- 'did not return an Array with the correct number of elements as the second argument of the callback'
- )
- }
-
- callbackUsed = true
-
- //---- Check that the elements are exactly the same as expected (ignoring order)
- exp.sort()
- list.sort()
- for (i = 0; i < exp.length; i++) {
- if (list[i] !== exp[i]) {
- return modfileError(
- 'did not return the correct list of files as the second argument of the callback.'
- )
- }
- }
-
- //WIN!!
- callback()
- })
- } catch (e) {
- return modfileError(
- 'threw an error:'
- + '\n\t' + util.inspect(e)
- )
- }
-
- setTimeout(function () {
- if (returned)
- return
-
- if (!callbackUsed)
- return modfileError('did not call the callback argument.')
- }, 300)
- }, 300)
-}
-
-function verifyModuleUsed (dir, trackFile, callback) {
- requires(trackFile, function (err, main, required) {
- if (err)
- return callback(err)
-
- if (required.length == 1) {
- console.log('\nYou got the correct answer but only used ' + bold('one') + ' file:')
- console.log('\t' + bold(red(required[0])))
- console.log(
- '\nThis problem requires you to use '
- + bold('one additional')
- + ' module file.\n'
- )
-
- return callback('bzzt!')
- }
-
- var modfile = required.filter(function (r) { return r != main })[0]
-
- validateModule(dir, trackFile, modfile, callback)
- })
-}
-
-function verify (dir, trackFile, callback) {
- onlyAsync(trackFile, function (err) {
- if (err)
- return callback(err)
-
- verifyModuleUsed(dir, trackFile, callback)
- })
-}
-
-module.exports = verify
View
24 problems/my_first_async_io/setup.js
@@ -1,24 +0,0 @@
-const boganipsum = require('boganipsum')
- , fs = require('fs')
- , path = require('path')
- , os = require('os')
- , onlyAsync = require('workshopper/verify-calls').verifyOnlyAsync
-
-module.exports = function () {
- var lines = Math.ceil(Math.random() * 50)
- , txt = boganipsum({ paragraphs: lines })
- , file = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.tmp')
- , trackFile = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.json')
-
- fs.writeFileSync(file, txt, 'utf8')
-
- return {
- args : [ file ]
- , stdin : null
- , modUseTrack : {
- trackFile : trackFile
- , modules : [ 'fs' ]
- }
- , verify : onlyAsync.bind(null, trackFile)
- }
-}
View
24 problems/my_first_io/setup.js
@@ -1,24 +0,0 @@
-const boganipsum = require('boganipsum')
- , fs = require('fs')
- , path = require('path')
- , os = require('os')
- , onlySync = require('workshopper/verify-calls').verifyOnlySync
-
-module.exports = function () {
- var lines = Math.ceil(Math.random() * 50)
- , txt = boganipsum({ paragraphs: lines })
- , file = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.tmp')
- , trackFile = path.join(os.tmpDir(), 'learnyounode_' + process.pid + '.json')
-
- fs.writeFileSync(file, txt, 'utf8')
-
- return {
- args : [ file ]
- , stdin : null
- , modUseTrack : {
- trackFile : trackFile
- , modules : [ 'fs' ]
- }
- , verify : onlySync.bind(null, trackFile)
- }
-}
Please sign in to comment.
Something went wrong with that request. Please try again.