{"action":"created","comment":{"url":"https://api.github.com/repos/apiaryio/dredd/pulls/comments/50234918","id":50234918,"diff_hunk":"@@ -1,295 +1,290 @@\n-hooks = require 'hooks'\n net = require 'net'\n-EventEmitter = require('events').EventEmitter\n \n-child_process = require('child_process')\n+{EventEmitter} = require 'events'\n+child_process = require 'child_process'\n+\n spawn = child_process.spawn\n \n # for stubbing in tests\n-console = require 'console'\n-\n+logger = require './logger'\n which = require './which'\n \n HOOK_TIMEOUT = 5000\n+\n+CONNECT_TIMEOUT = 1500\n+CONNECT_RETRY = 500\n+AFTER_CONNECT_WAIT = 100\n+\n+TERM_TIMEOUT = 5000\n+TERM_RETRY = 500\n+\n HANDLER_HOST = 'localhost'\n HANDLER_PORT = 61321\n HANDLER_MESSAGE_DELIMITER = \"\\n\"\n \n \n-emitter = new EventEmitter\n+class HooksWorkerClient\n+  constructor: (@runner) ->\n+    @language = @runner.hooks?.configuration?.options?.language\n+    @clientConnected = false\n+    @handlerEnded = false\n+    @handlerKilledIntentionally = false\n+    @connectError = false\n+    @emitter = new EventEmitter\n+\n+  start: (callback) ->\n+    @setCommandAndCheckForExecutables (executablesError) =>\n+      return callback(executablesError) if executablesError\n+      @spawnHandler (spawnHandlerError) =>\n+        return callback(spawnHandlerError) if spawnHandlerError\n+\n+        @connectToHandler (connectHandlerError) =>\n+          return callback(connectHandlerError) if connectHandlerError\n+\n+          @registerHooks (registerHooksError) =>\n+            return callback(registerHooksError) if registerHooksError\n+            callback()\n+\n+  stop: (callback) ->\n+    @disconnectFromHandler()\n+    @terminateHandler () ->\n+      callback()\n+\n+  terminateHandler: (callback) ->\n+    start = Date.now()\n+    logger.info 'Sending SIGTERM to the hooks handler'\n+    @handler.kill 'SIGTERM'\n+\n+    timeout = null\n+\n+    waitForHandlerTermOrKill = () =>\n+      if @handlerEnded == true\n+        clearTimeout(timeout)\n+        callback()\n+      else\n+        if (Date.now() - start) < TERM_TIMEOUT\n+          logger.info 'Sending SIGTERM to the hooks handler'\n+          @handler.kill 'SIGTERM'\n+        else\n+          @handlerKilledIntentionally = true\n+          logger.info 'Killing the hooks handler'\n+          @handler.kill 'SIGKILL'\n \n-language = hooks?.configuration?.options?.language\n+        timeout = setTimeout waitForHandlerTermOrKill, TERM_RETRY\n \n-# Select handler based on option, use option string as command if not match anything\n-if language == 'ruby'\n-  handlerCommand = 'dredd-hooks-ruby'\n-  unless which.which handlerCommand\n-    console.log \"Ruby hooks handler server command not found: #{handlerCommand}\"\n-    console.log \"Install ruby hooks handler by running:\"\n-    console.log \"$ gem install dredd_hooks\"\n-    hooks.processExit 1\n+    timeout = setTimeout waitForHandlerTermOrKill, TERM_RETRY\n \n-else if language == 'python'\n-  handlerCommand = 'dredd-hooks-python'\n-  unless which.which handlerCommand\n-    console.log \"Python hooks handler server command not found: #{handlerCommand}\"\n-    console.log \"Install python hooks handler by running:\"\n-    console.log \"$ pip install dredd_hooks\"\n-    hooks.processExit 1\n+  disconnectFromHandler: () ->\n+    @handlerClient.destroy()\n \n-else if language == 'nodejs'\n-  throw new Error 'Hooks handler should not be used for nodejs. Use Dredds\\' native node hooks instead'\n-else\n-  handlerCommand = language\n-  unless which.which handlerCommand\n-    console.log \"Hooks handler server command not found: #{handlerCommand}\"\n-    hooks.processExit 1\n+  setCommandAndCheckForExecutables: (callback) ->\n+    # Select handler based on option, use option string as command if not match anything\n+    if @language == 'ruby'\n+      @handlerCommand = 'dredd-hooks-ruby'\n+      unless which.which @handlerCommand\n+        msg = \"\"\"Ruby hooks handler server command not found: #{@handlerCommand}\n+        Install ruby hooks handler by running:\n+        $ gem install dredd_hooks\"\"\"\n \n+        error = new Error msg\n \n+        return callback(error)\n \n-pathGlobs = [].concat hooks?.configuration?.options?.hookfiles\n+    else if @language == 'python'\n+      @handlerCommand = 'dredd-hooks-python'\n+      unless which.which @handlerCommand\n+        msg = \"\"\"Python hooks handler server command not found: #{@handlerCommand}\n+              Install python hooks handler by running:\n+              $ pip install dredd_hooks\"\"\"\n \n-handler = spawn handlerCommand, pathGlobs\n+        error = new Error msg\n \n-console.log \"Spawning `#{language}` hooks handler\"\n+        return callback(error)\n \n-handler.stdout.on 'data', (data) ->\n-  console.log \"Hook handler stdout:\", data.toString()\n+    else if @language == 'nodejs'\n+      msg = 'Hooks handler should not be used for nodejs. Use Dredds\\' native node hooks instead'\n+      error = new Error msg\n+      return callback(error)\n \n-handler.stderr.on 'data', (data) ->\n-  console.log \"Hook handler stderr:\", data.toString()\n+    else\n+      @handlerCommand = @language\n+      if ! which.which @handlerCommand\n+        msg = \"Hooks handler server command not found: #{@handlerCommand}\"\n \n-handler.on 'close', (status) ->\n-  console.log \"Hook handler closed with status: #{status}\"\n-  if status? and status != 0\n-    hooks.processExit 2\n+        error = new Error msg\n+        return callback(error)\n \n-handler.on 'error', (error) ->\n-  console.log error\n+      else\n+        callback()\n \n-# Wait before connecting to a handler\n-# Hack for blocking sleep, loading of hooks in dredd is not async\n-# TODO Move connecting to handler to async beforeAll hook\n-now = new Date().getTime()\n-while new Date().getTime() < now + 1000\n-  true\n+  spawnHandler: (callback) ->\n \n-handlerClient = net.connect port: HANDLER_PORT, host: HANDLER_HOST, () ->\n-  # Do something when dredd starts\n-  # message =\n-  #   event: 'hooksInit'\n+    pathGlobs = [].concat @runner.hooks?.configuration?.options?.hookfiles\n \n-  # handler.write JSON.stringify message\n-  # handler.write HANDLER_MESSAGE_DELIMITER\n+    @handler = child_process.spawn @handlerCommand, pathGlobs\n \n-handlerClient.on 'error', (error) ->\n-  console.log 'Error connecting to the hook handler. Is the handler running?'\n-  console.log error\n-  hooks.processExit(3)\n+    logger.info \"Spawning `#{@language}` hooks handler\"\n \n-handlerBuffer = \"\"\n+    @handler.stdout.on 'data', (data) ->\n+      logger.info \"Hook handler stdout:\", data.toString()\n \n-handlerClient.on 'data', (data) ->\n-  handlerBuffer += data.toString()\n-  if data.toString().indexOf(HANDLER_MESSAGE_DELIMITER) > -1\n-    splittedData = handlerBuffer.split(HANDLER_MESSAGE_DELIMITER)\n+    @handler.stderr.on 'data', (data) ->\n+      logger.info \"Hook handler stderr:\", data.toString()\n \n-    # add last chunk to the buffer\n-    handlerBuffer = splittedData.pop()\n+    @handler.on 'exit', (status) =>\n+      # If killed there's no exit status code\n+      if status? and status != 0\n+        msg = \"Hook handler '#{@handlerCommand}' exited with status: #{status}\"\n+        error = new Error msg\n+        @runner.hookHandlerError = error\n \n-    messages = []\n-    for message in splittedData\n-      messages.push JSON.parse message\n+      else if !status?\n+        if @handlerKilledIntentionally == false\n+          msg = \"Hook handler '#{@handlerCommand}' was killed\"\n+          console.log msg\n+          error = new Error msg\n+          @runner.hookHandlerError = error\n \n-    for message in messages\n-      if message.uuid?\n-        emitter.emit message.uuid, message\n-      else\n-        console.log 'UUID not present in message: ', JSON.stringify(message, null ,2)\n-\n-# Wait before starting a test\n-# Hack for blocking sleep, loading of hooks in dredd is not async\n-# TODO Move connecting to handler to async beforeAll hook\n-now = new Date().getTime()\n-while new Date().getTime() < now + 1000\n-  true\n-\n-hooks.beforeEach (transaction, callback) ->\n-  # avoiding dependency on external module here.\n-  uuid = Date.now().toString() + '-' + Math. random().toString(36).substring(7)\n-\n-  # send transaction to the handler\n-  message =\n-    event: 'beforeEach'\n-    uuid: uuid\n-    data: transaction\n-\n-  handlerClient.write JSON.stringify message\n-  handlerClient.write HANDLER_MESSAGE_DELIMITER\n-\n-  # register event for the sent transaction\n-  messageHandler = (receivedMessage) ->\n-    clearTimeout timeout\n-    # workaround for assigning transaction\n-    # this does not work:\n-    # transaction = receivedMessage.data\n-    for key, value of receivedMessage.data\n-      transaction[key] = value\n-    callback()\n+      @handlerEnded = true\n+\n+    @handler.on 'error', (error) =>\n+      @runner.hookHandlerError = @handlerEnded = error\n \n-  handleTimeout = () ->\n-    transaction.fail = 'Hook timed out.'\n-    emitter.removeListener uuid, messageHandler\n     callback()\n \n-  # set timeout for the hook\n-  timeout = setTimeout handleTimeout, HOOK_TIMEOUT\n+  connectToHandler: (callback) ->\n+    start = Date.now()\n+    waitForConnect = () =>\n+      if (Date.now() - start) < CONNECT_TIMEOUT\n+        clearTimeout(timeout)\n \n-  emitter.on uuid, messageHandler\n+        if @connectError != false\n+          msg = 'Error connecting to the hook handler. Is the handler running? Retrying...'\n+          logger.warn msg\n \n-hooks.beforeEachValidation (transaction, callback) ->\n-  # avoiding dependency on external module here.\n-  uuid = Date.now().toString() + '-' + Math. random().toString(36).substring(7)\n+          @connectError = false\n \n-  # send transaction to the handler\n-  message =\n-    event: 'beforeEachValidation'\n-    uuid: uuid\n-    data: transaction\n+        if @clientConnected != true\n+          connectAndSetupClient()\n+          timeout = setTimeout waitForConnect, CONNECT_RETRY\n \n-  handlerClient.write JSON.stringify message\n-  handlerClient.write HANDLER_MESSAGE_DELIMITER\n+      else\n+        clearTimeout(timeout)\n+        if ! @clientConnected\n+          @handlerClient.destroy() if @handlerClient?\n+          msg = \"Connect timeout #{CONNECT_TIMEOUT / 1000}s to the handler \" +\n+          \"on #{HANDLER_HOST}:#{HANDLER_PORT} exceeded, try increasing the limit.\"\n+          error = new Error msg\n+          callback(error)\n \n-  # register event for the sent transaction\n-  messageHandler = (receivedMessage) ->\n-    clearTimeout timeout\n-    # workaround for assigning transaction\n-    # this does not work:\n-    # transaction = receivedMessage.data\n-    for key, value of receivedMessage.data\n-      transaction[key] = value\n-    callback()\n+    connectAndSetupClient = () =>\n+      if @runner.hookHandlerError?\n+        return callback(@runner.hookHandlerError)\n+\n+      @handlerClient = net.connect port: HANDLER_PORT, host: HANDLER_HOST\n \n-  handleTimeout = () ->\n-    transaction.fail = 'Hook timed out.'\n-    emitter.removeListener uuid, messageHandler\n-    callback()\n+      @handlerClient.on 'connect', () =>\n+        logger.info \"Connected to the hook handler, waiting #{AFTER_CONNECT_WAIT / 1000}s to start testing.\"\n+        @clientConnected = true\n+        clearTimeout(timeout)\n+        setTimeout callback, AFTER_CONNECT_WAIT\n \n-  # set timeout for the hook\n-  timeout = setTimeout handleTimeout, HOOK_TIMEOUT\n+      @handlerClient.on 'close', () ->\n \n-  emitter.on uuid, messageHandler\n+      @handlerClient.on 'error', (connectError) =>\n+        @connectError = connectError\n \n+      handlerBuffer = \"\"\n \n-hooks.afterEach (transaction, callback) ->\n-  # avoiding dependency on external module here.\n-  uuid = Date.now().toString() + '-' + Math. random().toString(36).substring(7)\n+      @handlerClient.on 'data', (data) =>\n+        handlerBuffer += data.toString()\n+        if data.toString().indexOf(HANDLER_MESSAGE_DELIMITER) > -1\n+          splittedData = handlerBuffer.split(HANDLER_MESSAGE_DELIMITER)\n \n-  # send transaction to the handler\n-  message =\n-    event: 'afterEach'\n-    uuid: uuid\n-    data: transaction\n+          # add last chunk to the buffer\n+          handlerBuffer = splittedData.pop()\n \n-  handlerClient.write JSON.stringify message\n-  handlerClient.write HANDLER_MESSAGE_DELIMITER\n+          messages = []\n+          for message in splittedData\n+            messages.push JSON.parse message\n \n-  # register event for the sent transaction\n-  messageHandler = (receivedMessage) ->\n-    clearTimeout timeout\n-    # workaround for assigning transaction\n-    # this does not work:\n-    # transaction = receivedMessage.data\n-    for key, value of receivedMessage.data\n-      transaction[key] = value\n-    callback()\n+          for message in messages\n+            if message.uuid?\n+              @emitter.emit message.uuid, message\n+            else\n+              logger.log 'UUID not present in message: ', JSON.stringify(message, null ,2)\n \n-  handleTimeout = () ->\n-    transaction.fail = 'Hook timed out.'\n-    emitter.removeListener uuid, messageHandler\n-    callback()\n+    timeout = setTimeout waitForConnect, CONNECT_RETRY\n \n-  # set timeout for the hook\n-  timeout = setTimeout handleTimeout, HOOK_TIMEOUT\n+  registerHooks: (callback) ->\n+    eachHookNames = [\n+      'beforeEach'\n+      'beforeEachValidation'\n+      'afterEach'\n+      'beforeAll'\n+      'afterAll'\n+    ]\n \n-  emitter.on uuid, messageHandler\n+    for eventName in eachHookNames then do (eventName) =>\n+      @runner.hooks[eventName] (data, hookCallback) =>\n+        # avoiding dependency on external module here.\n+        uuid = Date.now().toString() + '-' + Math. random().toString(36).substring(7)","path":"src/hooks-worker-client.coffee","position":392,"original_position":392,"commit_id":"358ea0fbfb42430d8bd393559e47b22e41770fa7","original_commit_id":"358ea0fbfb42430d8bd393559e47b22e41770fa7","user":{"login":"honzajavorek","id":283441,"avatar_url":"https://avatars.githubusercontent.com/u/283441?v=3","gravatar_id":"","url":"https://api.github.com/users/honzajavorek","html_url":"https://github.com/honzajavorek","followers_url":"https://api.github.com/users/honzajavorek/followers","following_url":"https://api.github.com/users/honzajavorek/following{/other_user}","gists_url":"https://api.github.com/users/honzajavorek/gists{/gist_id}","starred_url":"https://api.github.com/users/honzajavorek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/honzajavorek/subscriptions","organizations_url":"https://api.github.com/users/honzajavorek/orgs","repos_url":"https://api.github.com/users/honzajavorek/repos","events_url":"https://api.github.com/users/honzajavorek/events{/privacy}","received_events_url":"https://api.github.com/users/honzajavorek/received_events","type":"User","site_admin":false},"body":"should be external module :-P","created_at":"2016-01-20T10:03:54Z","updated_at":"2016-01-20T10:03:54Z","html_url":"https://github.com/apiaryio/dredd/pull/332#discussion_r50234918","pull_request_url":"https://api.github.com/repos/apiaryio/dredd/pulls/332","_links":{"self":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/comments/50234918"},"html":{"href":"https://github.com/apiaryio/dredd/pull/332#discussion_r50234918"},"pull_request":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/332"}}},"pull_request":{"url":"https://api.github.com/repos/apiaryio/dredd/pulls/332","id":54639991,"html_url":"https://github.com/apiaryio/dredd/pull/332","diff_url":"https://github.com/apiaryio/dredd/pull/332.diff","patch_url":"https://github.com/apiaryio/dredd/pull/332.patch","issue_url":"https://api.github.com/repos/apiaryio/dredd/issues/332","number":332,"state":"open","locked":false,"title":"Big refactoring and optimisation of hook handler spawner and client","user":{"login":"netmilk","id":79609,"avatar_url":"https://avatars.githubusercontent.com/u/79609?v=3","gravatar_id":"","url":"https://api.github.com/users/netmilk","html_url":"https://github.com/netmilk","followers_url":"https://api.github.com/users/netmilk/followers","following_url":"https://api.github.com/users/netmilk/following{/other_user}","gists_url":"https://api.github.com/users/netmilk/gists{/gist_id}","starred_url":"https://api.github.com/users/netmilk/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/netmilk/subscriptions","organizations_url":"https://api.github.com/users/netmilk/orgs","repos_url":"https://api.github.com/users/netmilk/repos","events_url":"https://api.github.com/users/netmilk/events{/privacy}","received_events_url":"https://api.github.com/users/netmilk/received_events","type":"User","site_admin":false},"body":"# * * *  WIP DO NOT MERGE * * * \r\n\r\n- [x] Removed proxyquire hell, using clean, asynchronous code (#235)\r\n- [x] Gracefully handling handler crashes and kills and lot of corner-cases (#238)\r\n- [x] Removed console.logging using logger or error messages in hook handler spawner\r\n- [x] Trying to terminate server first, then killing, waiting for exit. Should prevent leaking server process.\r\n- [x] All spawn, wait and kill variables in constants for future configuration from CLI (#328, #235)\r\n- [x] Gracefully ending the server event though Dredd experiences exception (#330)\r\n- [x] Flaky tests workaround, hopefully. (#282)\r\n- [ ] Tested against each hook handler test suites\r\n- [x] Removed stack tracing for non-exceptions in CLI\r\n- [x] 2x faster test-suite\r\n\r\n- [x] Rebased into one commit\r\n- [x] Rebased against master\r\n\r\nbefore:\r\n```\r\n  734 passing (4m)\r\n\r\n\r\nreal    4m20.975s\r\nuser    2m24.796s\r\nsys     0m5.835s\r\n```\r\n\r\nafter:\r\n```\r\n  820 passing (2m)\r\n\r\n\r\nreal\t2m15.007s\r\nuser\t0m43.701s\r\nsys     0m5.653s\r\n```","created_at":"2015-12-28T08:44:39Z","updated_at":"2016-01-20T10:03:54Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3b7f392c3fe95c4c0c2983eb0e97daefa5a8f7a1","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/apiaryio/dredd/pulls/332/commits","review_comments_url":"https://api.github.com/repos/apiaryio/dredd/pulls/332/comments","review_comment_url":"https://api.github.com/repos/apiaryio/dredd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/apiaryio/dredd/issues/332/comments","statuses_url":"https://api.github.com/repos/apiaryio/dredd/statuses/358ea0fbfb42430d8bd393559e47b22e41770fa7","head":{"label":"apiaryio:netmilk/proxyquireless-hooks-handler-client","ref":"netmilk/proxyquireless-hooks-handler-client","sha":"358ea0fbfb42430d8bd393559e47b22e41770fa7","user":{"login":"apiaryio","id":765943,"avatar_url":"https://avatars.githubusercontent.com/u/765943?v=3","gravatar_id":"","url":"https://api.github.com/users/apiaryio","html_url":"https://github.com/apiaryio","followers_url":"https://api.github.com/users/apiaryio/followers","following_url":"https://api.github.com/users/apiaryio/following{/other_user}","gists_url":"https://api.github.com/users/apiaryio/gists{/gist_id}","starred_url":"https://api.github.com/users/apiaryio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apiaryio/subscriptions","organizations_url":"https://api.github.com/users/apiaryio/orgs","repos_url":"https://api.github.com/users/apiaryio/repos","events_url":"https://api.github.com/users/apiaryio/events{/privacy}","received_events_url":"https://api.github.com/users/apiaryio/received_events","type":"Organization","site_admin":false},"repo":{"id":12590052,"name":"dredd","full_name":"apiaryio/dredd","owner":{"login":"apiaryio","id":765943,"avatar_url":"https://avatars.githubusercontent.com/u/765943?v=3","gravatar_id":"","url":"https://api.github.com/users/apiaryio","html_url":"https://github.com/apiaryio","followers_url":"https://api.github.com/users/apiaryio/followers","following_url":"https://api.github.com/users/apiaryio/following{/other_user}","gists_url":"https://api.github.com/users/apiaryio/gists{/gist_id}","starred_url":"https://api.github.com/users/apiaryio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apiaryio/subscriptions","organizations_url":"https://api.github.com/users/apiaryio/orgs","repos_url":"https://api.github.com/users/apiaryio/repos","events_url":"https://api.github.com/users/apiaryio/events{/privacy}","received_events_url":"https://api.github.com/users/apiaryio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/apiaryio/dredd","description":"Language agnostic HTTP API Testing Framework","fork":false,"url":"https://api.github.com/repos/apiaryio/dredd","forks_url":"https://api.github.com/repos/apiaryio/dredd/forks","keys_url":"https://api.github.com/repos/apiaryio/dredd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/apiaryio/dredd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/apiaryio/dredd/teams","hooks_url":"https://api.github.com/repos/apiaryio/dredd/hooks","issue_events_url":"https://api.github.com/repos/apiaryio/dredd/issues/events{/number}","events_url":"https://api.github.com/repos/apiaryio/dredd/events","assignees_url":"https://api.github.com/repos/apiaryio/dredd/assignees{/user}","branches_url":"https://api.github.com/repos/apiaryio/dredd/branches{/branch}","tags_url":"https://api.github.com/repos/apiaryio/dredd/tags","blobs_url":"https://api.github.com/repos/apiaryio/dredd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/apiaryio/dredd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/apiaryio/dredd/git/refs{/sha}","trees_url":"https://api.github.com/repos/apiaryio/dredd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/apiaryio/dredd/statuses/{sha}","languages_url":"https://api.github.com/repos/apiaryio/dredd/languages","stargazers_url":"https://api.github.com/repos/apiaryio/dredd/stargazers","contributors_url":"https://api.github.com/repos/apiaryio/dredd/contributors","subscribers_url":"https://api.github.com/repos/apiaryio/dredd/subscribers","subscription_url":"https://api.github.com/repos/apiaryio/dredd/subscription","commits_url":"https://api.github.com/repos/apiaryio/dredd/commits{/sha}","git_commits_url":"https://api.github.com/repos/apiaryio/dredd/git/commits{/sha}","comments_url":"https://api.github.com/repos/apiaryio/dredd/comments{/number}","issue_comment_url":"https://api.github.com/repos/apiaryio/dredd/issues/comments{/number}","contents_url":"https://api.github.com/repos/apiaryio/dredd/contents/{+path}","compare_url":"https://api.github.com/repos/apiaryio/dredd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/apiaryio/dredd/merges","archive_url":"https://api.github.com/repos/apiaryio/dredd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/apiaryio/dredd/downloads","issues_url":"https://api.github.com/repos/apiaryio/dredd/issues{/number}","pulls_url":"https://api.github.com/repos/apiaryio/dredd/pulls{/number}","milestones_url":"https://api.github.com/repos/apiaryio/dredd/milestones{/number}","notifications_url":"https://api.github.com/repos/apiaryio/dredd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/apiaryio/dredd/labels{/name}","releases_url":"https://api.github.com/repos/apiaryio/dredd/releases{/id}","created_at":"2013-09-04T12:21:35Z","updated_at":"2016-01-19T21:28:17Z","pushed_at":"2016-01-18T16:46:15Z","git_url":"git://github.com/apiaryio/dredd.git","ssh_url":"git@github.com:apiaryio/dredd.git","clone_url":"https://github.com/apiaryio/dredd.git","svn_url":"https://github.com/apiaryio/dredd","homepage":"http://dredd.rtfd.org/","size":1330,"stargazers_count":730,"watchers_count":730,"language":"CoffeeScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":71,"mirror_url":null,"open_issues_count":94,"forks":71,"open_issues":94,"watchers":730,"default_branch":"master"}},"base":{"label":"apiaryio:master","ref":"master","sha":"f02a6b133bc50c93d8edc04b917130a1771d9ec5","user":{"login":"apiaryio","id":765943,"avatar_url":"https://avatars.githubusercontent.com/u/765943?v=3","gravatar_id":"","url":"https://api.github.com/users/apiaryio","html_url":"https://github.com/apiaryio","followers_url":"https://api.github.com/users/apiaryio/followers","following_url":"https://api.github.com/users/apiaryio/following{/other_user}","gists_url":"https://api.github.com/users/apiaryio/gists{/gist_id}","starred_url":"https://api.github.com/users/apiaryio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apiaryio/subscriptions","organizations_url":"https://api.github.com/users/apiaryio/orgs","repos_url":"https://api.github.com/users/apiaryio/repos","events_url":"https://api.github.com/users/apiaryio/events{/privacy}","received_events_url":"https://api.github.com/users/apiaryio/received_events","type":"Organization","site_admin":false},"repo":{"id":12590052,"name":"dredd","full_name":"apiaryio/dredd","owner":{"login":"apiaryio","id":765943,"avatar_url":"https://avatars.githubusercontent.com/u/765943?v=3","gravatar_id":"","url":"https://api.github.com/users/apiaryio","html_url":"https://github.com/apiaryio","followers_url":"https://api.github.com/users/apiaryio/followers","following_url":"https://api.github.com/users/apiaryio/following{/other_user}","gists_url":"https://api.github.com/users/apiaryio/gists{/gist_id}","starred_url":"https://api.github.com/users/apiaryio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apiaryio/subscriptions","organizations_url":"https://api.github.com/users/apiaryio/orgs","repos_url":"https://api.github.com/users/apiaryio/repos","events_url":"https://api.github.com/users/apiaryio/events{/privacy}","received_events_url":"https://api.github.com/users/apiaryio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/apiaryio/dredd","description":"Language agnostic HTTP API Testing Framework","fork":false,"url":"https://api.github.com/repos/apiaryio/dredd","forks_url":"https://api.github.com/repos/apiaryio/dredd/forks","keys_url":"https://api.github.com/repos/apiaryio/dredd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/apiaryio/dredd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/apiaryio/dredd/teams","hooks_url":"https://api.github.com/repos/apiaryio/dredd/hooks","issue_events_url":"https://api.github.com/repos/apiaryio/dredd/issues/events{/number}","events_url":"https://api.github.com/repos/apiaryio/dredd/events","assignees_url":"https://api.github.com/repos/apiaryio/dredd/assignees{/user}","branches_url":"https://api.github.com/repos/apiaryio/dredd/branches{/branch}","tags_url":"https://api.github.com/repos/apiaryio/dredd/tags","blobs_url":"https://api.github.com/repos/apiaryio/dredd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/apiaryio/dredd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/apiaryio/dredd/git/refs{/sha}","trees_url":"https://api.github.com/repos/apiaryio/dredd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/apiaryio/dredd/statuses/{sha}","languages_url":"https://api.github.com/repos/apiaryio/dredd/languages","stargazers_url":"https://api.github.com/repos/apiaryio/dredd/stargazers","contributors_url":"https://api.github.com/repos/apiaryio/dredd/contributors","subscribers_url":"https://api.github.com/repos/apiaryio/dredd/subscribers","subscription_url":"https://api.github.com/repos/apiaryio/dredd/subscription","commits_url":"https://api.github.com/repos/apiaryio/dredd/commits{/sha}","git_commits_url":"https://api.github.com/repos/apiaryio/dredd/git/commits{/sha}","comments_url":"https://api.github.com/repos/apiaryio/dredd/comments{/number}","issue_comment_url":"https://api.github.com/repos/apiaryio/dredd/issues/comments{/number}","contents_url":"https://api.github.com/repos/apiaryio/dredd/contents/{+path}","compare_url":"https://api.github.com/repos/apiaryio/dredd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/apiaryio/dredd/merges","archive_url":"https://api.github.com/repos/apiaryio/dredd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/apiaryio/dredd/downloads","issues_url":"https://api.github.com/repos/apiaryio/dredd/issues{/number}","pulls_url":"https://api.github.com/repos/apiaryio/dredd/pulls{/number}","milestones_url":"https://api.github.com/repos/apiaryio/dredd/milestones{/number}","notifications_url":"https://api.github.com/repos/apiaryio/dredd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/apiaryio/dredd/labels{/name}","releases_url":"https://api.github.com/repos/apiaryio/dredd/releases{/id}","created_at":"2013-09-04T12:21:35Z","updated_at":"2016-01-19T21:28:17Z","pushed_at":"2016-01-18T16:46:15Z","git_url":"git://github.com/apiaryio/dredd.git","ssh_url":"git@github.com:apiaryio/dredd.git","clone_url":"https://github.com/apiaryio/dredd.git","svn_url":"https://github.com/apiaryio/dredd","homepage":"http://dredd.rtfd.org/","size":1330,"stargazers_count":730,"watchers_count":730,"language":"CoffeeScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":71,"mirror_url":null,"open_issues_count":94,"forks":71,"open_issues":94,"watchers":730,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/332"},"html":{"href":"https://github.com/apiaryio/dredd/pull/332"},"issue":{"href":"https://api.github.com/repos/apiaryio/dredd/issues/332"},"comments":{"href":"https://api.github.com/repos/apiaryio/dredd/issues/332/comments"},"review_comments":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/332/comments"},"review_comment":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/apiaryio/dredd/pulls/332/commits"},"statuses":{"href":"https://api.github.com/repos/apiaryio/dredd/statuses/358ea0fbfb42430d8bd393559e47b22e41770fa7"}}}}