[Pkg-javascript-commits] [node-assertive] 01/02: Imported Upstream version 2.1.1

Ross Gammon ross-guest at moszumanska.debian.org
Tue Dec 20 18:32:29 UTC 2016


This is an automated email from the git hooks/post-receive script.

ross-guest pushed a commit to branch master
in repository node-assertive.

commit ff7659640d30bca7e624a1866376f4344c8d5c46
Author: Ross Gammon <rossgammon at mail.dk>
Date:   Sat Nov 19 19:20:42 2016 +0100

    Imported Upstream version 2.1.1
---
 .editorconfig                |  11 +
 .gitignore                   |   5 +
 .npmrc                       |   1 +
 .travis.yml                  |  16 ++
 CHANGELOG.md                 |  27 ++
 CONTRIBUTING.md              | 147 ++++++++++
 LICENSE                      |  29 ++
 README.md                    | 214 ++++++++++++++
 coffeelint.json              | 122 ++++++++
 lib/assertive.js             | 504 +++++++++++++++++++++++++++++++++
 package.json                 |  61 ++++
 src/assertive.coffee         | 390 +++++++++++++++++++++++++
 test/assertive.test.coffee   | 661 +++++++++++++++++++++++++++++++++++++++++++
 test/mocha.opts              |   4 +
 test/promisified.test.coffee |  96 +++++++
 15 files changed, 2288 insertions(+)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..beffa30
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ddbce9a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+npm-debug.log
+/tmp
+*.log
+*.gz
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..38f11c6
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+registry=https://registry.npmjs.org
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b1a0247
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: node_js
+node_js:
+  - '0.10'
+  - '4'
+before_install:
+  - npm install -g npm at latest-2
+before_deploy:
+  - 'git config --global user.email "opensource at groupon.com"'
+  - 'git config --global user.name "Groupon"'
+deploy:
+  provider: script
+  script: ./node_modules/.bin/nlm release
+  skip_cleanup: true
+  'on':
+    branch: master
+    node: '4'
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..30a6809
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,27 @@
+### 2.1.1
+
+* chore: fix who travis credits for auto-commits - **[@dbushong](https://github.com/dbushong)** [#27](https://github.com/groupon/assertive/pull/27)
+  - [`0ff64b5`](https://github.com/groupon/assertive/commit/0ff64b53cdbdcbe82fe6cf5df21a0255305c0300) **chore:** fix who travis credits for auto-commits
+
+
+### 2.1.0
+
+* feat: integrate assertive-as-promised functionality - **[@dbushong](https://github.com/dbushong)** [#26](https://github.com/groupon/assertive/pull/26)
+  - [`c9be116`](https://github.com/groupon/assertive/commit/c9be1165235696c773b50b0b80e15ec6bb143633) **feat:** integrate assertive-as-promised functionality
+
+
+### 2.0.3
+
+* Apply latest nlm generator - **[@i-tier-bot](https://github.com/i-tier-bot)** [#25](https://github.com/groupon/assertive/pull/25)
+  - [`1eb5321`](https://github.com/groupon/assertive/commit/1eb5321691d6ca51287ded93d1ac00bee5037baa) **chore:** Apply latest nlm generator
+  - [`e4939c3`](https://github.com/groupon/assertive/commit/e4939c385b9ca79c0476918b43ae0c917e929004) **fix:** Use lodash 4 method names
+
+
+2.0.2
+-----
+* Change Fn.bind to _.partial to support non-ES5 environments - @rduenasf #23
+
+2.0.1
+-----
+* add coffeelint & npub - @chkhoo #22
+* replace underscore & redux with lodash & coffee-script - @chkhoo #21
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..cfd3015
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,147 @@
+<!-- Generated by generator-nlm -->
+
+# Contributing
+
+🎉🏅 Thanks for helping us improve this project! 🙏
+
+This document outlines some of the practices we care about.
+If you have any questions or suggestions about the process,
+feel free to [open an issue](#reporting-issues)
+.
+## How Can I Contribute?
+
+### Reporting Issues
+
+If you find any mistakes in the docs or a bug in the code,
+please [open an issue in Github](https://github.com/groupon/assertive/issues/new) so we can look into it.
+You can also [create a PR](#contributing-code) fixing it yourself, or course.
+
+If you report a bug, please follow these guidelines:
+
+* Make sure the bug exists in the latest version.
+* Include instructions on how to reproduce the issue.
+  The instructions should be as minimal as possible
+  and answer the three big questions:
+  1. What are the exact steps you took? This includes the exact versions of node, npm, and any packages involved.
+  1. What result are you expecting?
+  1. What is the actual result?
+
+### Improving Documentation
+
+For small documentation changes, you can use [Github's editing feature](https://help.github.com/articles/editing-files-in-another-user-s-repository/).
+The only thing to keep in mind is to prefix the commit message with "docs: ".
+The detault commit message generated by Github will lead to a failing CI build.
+
+For larger updates to the documentation
+it might be better to follow the [instructions for contributing code below](#contributing-code).
+
+### Contributing Code
+
+**Note:** If you're planning on making substantial changes,
+please [open an issue first to discuss your idea](#reporting-issues).
+Otherwise you might end up investing a lot of work
+only to discover that it conflicts with plans the maintainers might have.
+
+The general steps for creating a pull request are:
+
+1. Create a branch for your change.
+   Always start your branch from the latest `master`.
+   We often prefix the branch name with our initials, e.g. `jk-a-change`.
+1. Run `npm install` to install the dependencies.
+1. If you're fixing a bug, be sure to write a test *first*.
+   That way you can validate that the test actually catches the bug and doesn't pass.
+1. Make your changes to the code.
+   Remember to update the tests if you add new features or change behavior. 
+1. Run the tests via `npm test`. This will also run style checks and other validations.
+   You might see errors about uncommitted files.
+   This is expected until you commit your changes.
+1. Once you're done, `git add .` and `git commit`.
+   Please follow the [commit message conventions](#commits--commit-messages) described below.
+1. Push your branch to Github & create a PR.
+
+#### Code Style
+
+In addition to any linting rules the project might include,
+a few general rules of thumb:
+
+* Try to match the style of the rest of the code.
+* We prefer simple code that is easy to understand over terse, expressive code.
+* We try to structure projects by semantics instead of role.
+  E.g. we'd rather have a `tree.js` module that contains tree traversal-related helpers
+  than a `helpers.js` module.
+* Actually, if you create helpers you might want to put those into a separate package.
+  That way it's easier to reuse them.
+
+#### Commits & Commit Messages
+
+Please follow the [angular commit message conventions](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines).
+We use an automated tool for generating releases
+that depends on the conventions to determine the next version and the content of the changelog.
+Commit messages that don't follow the conventions will cause `npm test` (and thus CI) to fail.
+
+The short summary - a commit message should look like this:
+
+```
+<type>: <subject>
+
+<body>
+
+<references>
+
+<footer>
+```
+
+Everything but the first line is optional.
+The empty lines between the different parts are required.
+
+* `<type>`: One of the following:
+  - **feat:** Introduces a new feature. This will cause the minor version to go up.
+  - **fix:** A bug fix. Causes a patch version bump.
+  - **docs:** Changes to the documentation.
+    This will also cause an increase of the patch version so that the changes show up in the npm registry.
+  - **style:** Cleanup & lint rule fixes.
+    Note that often it's better to just amend the previous commit if it introduced lint errors.
+  - **refactor:** Changes to the code structure without fixing bugs or adding features.
+  - **perf:** Performance optimizations.
+  - **test:** Fixing existing tests or adding missing ones.
+    Just like with **style**, if you add tests to a feature you just introduced in the previous commit,
+    consider keeping the tests and the feature in the same commit instead.
+  - **chore:** Changes to the project setup and tools, dependency bumps, house-keeping.
+* `<subject>`: A [good git commit message subject](http://chris.beams.io/posts/git-commit/#limit-50).
+  - Keep it brief. If possible the whole first line should have at most 50 characters.
+  - Use imperative mood. "Create" instead of "creates" or "created".
+  - No period (".") at the end.
+* `<body>`: Motivation for the change and any context required for understanding the choices made.
+  Just like the subject, it should use imperative mood.
+* `<references>`: Any URLs relevant to the PR go here.
+  Use one line per URL and prefix it with the kind of relationship, e.g. "Closes: " or "See: ".
+  If you are referencing an issue in your commit body or PR description,
+  never use `#123` but the full URL to the issue or PR you are referencing.
+  That way the reference is easy to resolve from the git history without having to "guess" the correct link
+  even if the commit got cherry-picked or merged into a different project.
+* `<footer>`: This part only applies if your commit introduces a breaking change.
+  It's important this is present, otherwise the major version will not increase.
+  See below for an example.
+
+##### Examples
+
+A feature that introduces a breaking change:
+
+```
+feat: Support --yes CLI option
+
+For existing projects all prompts can be inferred automatically.
+Manual confirmation for each default provides no value in that case.
+
+Closes https://github.com/my/project/issues/123
+
+BREAKING CHANGE: This removes support for interactive password entry.
+Users will have to login beforehand.
+```
+
+A simple bug fix:
+
+```
+fix: Handle multi-byte characters in search logic
+```
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f8fda65
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2013, Groupon, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+Neither the name of GROUPON nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9b2dc4e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,214 @@
+assertive
+=========
+
+A terse, yet expressive assertion library
+
+Is Assertive different from other assertion libraries?
+----------------------------------------------------------------------
+
+Assertive aims to make the exact cause of breakage and intent of tests
+as fast and easy to spot as possible, with much attention paid to both
+the colour and alignment of expected and actual data, so you should be
+able to glean what you need immediately.
+
+It also tries to pre-empt false negative tests from ever happening, by
+rigorously testing for correct assertion invocation and by avoiding to
+pick names for assertions with a track record of being misinterpreted,
+not just by people reading the code, but also by programmers _writing_
+them, which can make even 100%-test-coverage code fail on behalf of it
+testing for the wrong thing.
+
+Semantic Versioning
+----------------------------------------------------------------------
+
+Assertive uses [semver](http://semver.org/) version numbers, though we
+should point out that we may tighten assertion checks in minor version
+number updates, making code that previously silently passed, now fail.
+
+Case in point: before v1.3.0, code using an assertion to verify that a
+string included the empty string, would do just that. In other words -
+nothing, since that assertion does not test anything. Now, such a test
+is flagged as a bug in your test suite that you should fix, as that is
+not asserting something about your code, but about strings in general.
+
+In Assertive, breaking changes implying a major version bump, would be
+things like argument order changes. If you really do not want improved
+coverage against this type of error with a random minor version update
+you should pin a version you like in your `package.json` rather than a
+version range.
+
+Usage
+----------------------------------------------------------------------
+
+Each assertion lets you state a condition and an optional help message
+about what semantics your test asserts, which gets presented first, if
+the assertion fails. (This is generally much more useful than messages
+along the lines of "expected true to be false", especially when it may
+be hard to tell later what the intended purpose of a test really was.)
+
+Besides failing when what each assertion guards against, they also all
+fail if you pass too few, too many or otherwise illegal parameters, as
+when a tired programmer expects "expect" to compare the two parameters
+he passed in some way and trip when they mismatch, though all it would
+ever test is that the first was truthy. To not get test suites full of
+almost-no-op tests like that, Assertive fails straight away like this:
+
+```
+Expected: true
+Actually: 10
+```
+
+There have been test suites full of no-op tests similar to this, which
+have gone undetected for months or years, giving a false sense of what
+regressions you are guarded against.
+
+You may pass any of the functions an item to be tested as a promise,
+and it will be tested after the promise is resolved.  In this case, the
+test will return a promise which will be resolved or rejected as appropriate.
+A promise-aware test runner (e.g. [Mocha](https://mochajs.org/)
+version >= 1.18.0) is highly recommended.
+
+These docs show a typical invocation, and what you see when it failed:
+
+
+### `expect`
+```
+assert.expect(bool)
+# fail if bool != true
+```
+
+```
+expect '2 > 1', 2 > 1
+
+Assertion failed: 2 > 1
+```
+
+
+### `truthy`
+
+**Note**: Using `truthy` in your tests is a code smell.
+More often than not there is another, more precise test.
+Only use `truthy` when there is no way of knowing what the actual value will be.
+If `bool` is the result of a boolean operation, use `expect`.
+If `bool` is an unknown value, use `match` or `include` to narrow it down.
+
+```
+assert.truthy(bool)
+assert.truthy(explanation, bool)
+# fail if !bool
+```
+
+```
+truthy 'something was populated in the email field', form.email.value
+
+Assertion failed: something was populated in the email field
+expected undefined to be truthy
+```
+
+
+### `equal`
+```
+assert.equal(expected, actual)
+assert.equal(explanation, expected, actual)
+# fail unless actual === expected
+
+Assertion failed: decode the Epoch to 0s after Jan 1st, 1970
+Expected 86400000 to be
+equal to 0
+```
+
+### `deepEqual`
+```
+assert.deepEqual(expected, actual)
+assert.deepEqual(explanation, expected, actual)
+# fail unless _.isEqual(expected, actual)
+
+Assertion failed: ensure that all methods we tested were handled, and in the right order
+mismatch: {"methods":["GET"]} didn't
+deepEqual {"methods":["GET","POST","PUT","DELETE"]}
+```
+
+### `include`
+```
+assert.include(needle, haystack)
+assert.include(explanation, needle, haystack)
+# fail unless haystack has a substring needle, or _.include haystack, needle
+
+Assertion failed: only accept supported, case-normalized method names
+expected ["GET","POST","PUT","DELETE"]
+to include "get"
+```
+
+### `match`
+```
+assert.match(regexp, string)
+assert.match(explanation, regexp, needle)
+# fail unless regexp matches the given string, or regexp.test string
+
+Assertion failed: only affirmative pirate answers accepted
+Expected: /aye|yar+/
+to match: "nay"
+```
+
+### `throws`
+```
+err = assert.throws(functionThatThrows)
+err = assert.throws(explanation, functionThatThrows)
+# fail unless the provided functionThatThrows() calls throw
+# (on non-failures the return value is whatever was thrown)
+
+Assertion failed: ensure that bad inputs throw an error
+didn't throw an exception as expected to
+```
+
+### `hasType`
+```
+assert.hasType(<type>, value);
+assert.hasType(explanation, <type>, value);
+assert.hasType(null, value)
+assert.hasType(Date, value)
+assert.hasType(Array, value)
+assert.hasType(String, value)
+assert.hasType(RegExp, value)
+assert.hasType(Boolean, value)
+assert.hasType(Function, value)
+assert.hasType(Object, value)
+assert.hasType(NaN, value)
+assert.hasType(Number, value)
+assert.hasType(undefined, value)
+# fail unless _.isType(value) is true for given Type, or the
+# same test for a more specific type (listed above) was true
+```
+
+### `resolves`
+```
+promise = assert.resolves(promise)
+promise = assert.resolves(explanation, promise)
+# Wait for promise to resolve, then resolve if successful, reject otherwise
+# Always returns a promise, unless called with non-promise (not allowed)
+
+Assertion failed: should resolve to good stuff
+Promise was rejected despite resolves assertion:
+Timeout in 10000ms
+```
+
+### `rejects`
+```
+promiseForErr = assert.rejects(promise)
+promiseForErr = assert.rejects(explanation, promise)
+# Wait for promise to reject, resolve with error if it does, reject otherwise
+# Basically inverse of resolves(), but resolves with the error for more testing
+# Always returns a promise, unless called with non-promise (not allowed)
+
+Assertion failed: should reject after Timeout
+Promise wasn't rejected as expected to
+```
+
+### `falsey`, `notEqual`, `notDeepEqual`, `notInclude`, `notMatch`, `notThrows`, `notHasType`
+Versions of the above functions taking the same arguments, but asserting
+the opposite outcome. The assertion failure messages are just as helpful.
+
+License
+----------------------------------------------------------------------
+
+[BSD 3-Clause open source license](LICENSE)
diff --git a/coffeelint.json b/coffeelint.json
new file mode 100644
index 0000000..f7eeb24
--- /dev/null
+++ b/coffeelint.json
@@ -0,0 +1,122 @@
+{
+  "arrow_spacing": {
+    "level": "error"
+  },
+  "camel_case_classes": {
+    "level": "error"
+  },
+  "coffeescript_error": {
+    "level": "error"
+  },
+  "colon_assignment_spacing": {
+    "level": "error",
+    "spacing": {
+      "left": 0,
+      "right": 1
+    }
+  },
+  "cyclomatic_complexity": {
+    "value": 10,
+    "level": "ignore"
+  },
+  "duplicate_key": {
+    "level": "error"
+  },
+  "empty_constructor_needs_parens": {
+    "level": "ignore"
+  },
+  "ensure_comprehensions": {
+    "level": "ignore"
+  },
+  "indentation": {
+    "value": 2,
+    "level": "error"
+  },
+  "line_endings": {
+    "level": "error",
+    "value": "unix"
+  },
+  "max_line_length": {
+    "value": 80,
+    "level": "ignore",
+    "limitComments": false
+  },
+  "missing_fat_arrows": {
+    "level": "ignore"
+  },
+  "newlines_after_classes": {
+    "value": 1,
+    "level": "error"
+  },
+  "no_backticks": {
+    "level": "error"
+  },
+  "no_debugger": {
+    "level": "error"
+  },
+  "no_empty_functions": {
+    "level": "ignore"
+  },
+  "no_empty_param_list": {
+    "level": "error"
+  },
+  "no_implicit_braces": {
+    "level": "ignore",
+    "strict": true
+  },
+  "no_implicit_parens": {
+    "strict": true,
+    "level": "ignore"
+  },
+  "no_interpolation_in_single_quotes": {
+    "level": "error"
+  },
+  "no_plusplus": {
+    "level": "ignore"
+  },
+  "no_stand_alone_at": {
+    "level": "error"
+  },
+  "no_tabs": {
+    "level": "error"
+  },
+  "no_throwing_strings": {
+    "level": "ignore"
+  },
+  "no_trailing_semicolons": {
+    "level": "error"
+  },
+  "no_trailing_whitespace": {
+    "level": "error",
+    "allowed_in_comments": false,
+    "allowed_in_empty_lines": false
+  },
+  "no_unnecessary_double_quotes": {
+    "level": "error"
+  },
+  "no_unnecessary_fat_arrows": {
+    "level": "error"
+  },
+  "non_empty_constructor_needs_parens": {
+    "level": "ignore"
+  },
+  "prefer_english_operator": {
+    "level": "ignore",
+    "doubleNotLevel": "ignore"
+  },
+  "space_operators": {
+    "level": "error"
+  },
+  "spacing_after_comma": {
+    "level": "error"
+  },
+  "transform_messes_up_line_numbers": {
+    "level": "ignore"
+  },
+  "use_strict": {
+    "module": "coffeelint-use-strict",
+    "level": "error",
+    "allowGlobal": true,
+    "requireGlobal": true
+  }
+}
diff --git a/lib/assertive.js b/lib/assertive.js
new file mode 100644
index 0000000..b7d7117
--- /dev/null
+++ b/lib/assertive.js
@@ -0,0 +1,504 @@
+'use strict';
+
+/*
+Copyright (c) 2013, Groupon, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+Neither the name of GROUPON nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+var _, abbreviate, asRegExp, assert, assertSync, clear, error, fn, fn1, getNameOfType, getTypeName, global, green, handleArgs, i, implodeNicely, includes, isArray, isEqual, isFunction, isNumber, isPromiseAlike, isRegExp, isString, isType, len, map, name, nameNegative, positiveAssertions, red, ref, ref1, ref2, stringify, toString, type, types,
+  indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+  slice = [].slice,
+  hasProp = {}.hasOwnProperty;
+
+global = Function('return this')();
+
+ref1 = _ = (ref = global._) != null ? ref : require('lodash'), includes = ref1.includes, isEqual = ref1.isEqual, isString = ref1.isString, isNumber = ref1.isNumber, isRegExp = ref1.isRegExp, isArray = ref1.isArray, isFunction = ref1.isFunction, map = ref1.map;
+
+assertSync = {
+  truthy: function(bool) {
+    var explanation, name, negated, ref2;
+    ref2 = handleArgs(this, [1, 2], arguments, 'truthy'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 2) {
+      explanation = arguments[0], bool = arguments[1];
+    }
+    if (!((!!bool) ^ negated)) {
+      throw error("Expected " + (red(stringify(bool))) + " to be " + name, explanation);
+    }
+  },
+  expect: function(bool) {
+    var explanation;
+    if (arguments.length === 2) {
+      explanation = arguments[0], bool = arguments[1];
+    }
+    if (explanation) {
+      return assertSync.equal(explanation, true, bool);
+    } else {
+      return assertSync.equal(true, bool);
+    }
+  },
+  equal: function(expected, actual) {
+    var explanation, name, negated, ref2;
+    ref2 = handleArgs(this, [2, 3], arguments, 'equal'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 3) {
+      explanation = arguments[0], expected = arguments[1], actual = arguments[2];
+    }
+    if (negated) {
+      if (expected === actual) {
+        throw error('notEqual assertion expected ' + red(stringify(actual)) + ' to be exactly anything else', explanation);
+      }
+    } else if (expected !== actual) {
+      throw error("Expected: " + (green(stringify(expected))) + "\nActually: " + (red(stringify(actual))), explanation);
+    }
+  },
+  deepEqual: function(expected, actual) {
+    var explanation, message, name, negated, ref2, rightLooks, wrongLooks;
+    ref2 = handleArgs(this, [2, 3], arguments, 'deepEqual'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 3) {
+      explanation = arguments[0], expected = arguments[1], actual = arguments[2];
+    }
+    if (!(isEqual(expected, actual) ^ negated)) {
+      wrongLooks = stringify(actual);
+      if (negated) {
+        throw error("notDeepEqual assertion expected exactly anything else but\n" + (red(wrongLooks)), explanation);
+      }
+      rightLooks = stringify(expected);
+      message = wrongLooks === rightLooks ? "deepEqual " + (green(rightLooks)) + " failed on something that\nserializes to the same result (likely some function)" : "Expected: " + (green(rightLooks)) + "\nActually: " + (red(wrongLooks));
+      throw error(message, explanation);
+    }
+  },
+  include: function(needle, haystack) {
+    var contained, explanation, message, name, negated, problem, ref2, verb, what;
+    ref2 = handleArgs(this, [2, 3], arguments, 'include'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 3) {
+      explanation = arguments[0], needle = arguments[1], haystack = arguments[2];
+    }
+    if (isString(haystack)) {
+      if (needle === '') {
+        what = negated ? 'always-failing test' : 'no-op test';
+        throw error(what + " detected: all strings contain the empty string!");
+      }
+      if (!(isString(needle) || isNumber(needle) || isRegExp(needle))) {
+        problem = 'needs a RegExp/String/Number needle for a String haystack';
+        throw new TypeError(name + " " + problem + "; you used:\n" + name + " " + (green(stringify(haystack))) + ", " + (red(stringify(needle))));
+      }
+    } else if (!isArray(haystack)) {
+      needle = stringify(needle);
+      throw new TypeError(name + " takes a String or Array haystack; you used:\n" + name + " " + (red(stringify(haystack))) + ", " + needle);
+    }
+    if (isString(haystack)) {
+      if (isRegExp(needle)) {
+        contained = haystack.match(needle);
+      } else {
+        contained = haystack.indexOf(needle) !== -1;
+      }
+    } else {
+      contained = includes(haystack, needle);
+    }
+    verb = isRegExp(needle) ? 'match' : 'include';
+    if (negated) {
+      if (contained) {
+        message = "notInclude expected needle not to be found in haystack\n- needle: " + (stringify(needle)) + "\nhaystack: " + (abbreviate('', haystack));
+        if (isString(haystack) && isRegExp(needle)) {
+          message += ', but found:\n';
+          if (needle.global) {
+            message += contained.map(function(s) {
+              return "* " + (red(stringify(s)));
+            }).join('\n');
+          } else {
+            message += "* " + (red(stringify(contained[0])));
+          }
+        }
+        throw error(message, explanation);
+      }
+    } else if (!contained) {
+      throw error(name + " expected needle to be found in haystack\n- needle: " + (stringify(needle)) + "\nhaystack: " + (abbreviate('', haystack)), explanation);
+    }
+  },
+  match: function(regexp, string) {
+    var called, count, explanation, matched, message, name, negated, oops, re, ref2;
+    ref2 = handleArgs(this, [2, 3], arguments, 'match'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 3) {
+      explanation = arguments[0], regexp = arguments[1], string = arguments[2];
+    }
+    if (!((re = isRegExp(regexp)) && isString(string))) {
+      string = abbreviate('string', string);
+      called = name + " " + (stringify(regexp)) + ", " + (red(string));
+      if (!re) {
+        oops = 'regexp arg is not a RegExp';
+      } else {
+        oops = 'string arg is not a String';
+      }
+      throw new TypeError(name + ": " + oops + "; you used:\n" + called);
+    }
+    matched = regexp.test(string);
+    if (negated) {
+      if (!matched) {
+        return;
+      }
+      message = "Expected: " + (stringify(regexp)) + "\nnot to match: " + (red(abbreviate('string', string)));
+      if (regexp.global) {
+        count = string.match(regexp).length;
+        message += "\nMatches: " + (red(count));
+      }
+      throw error(message, explanation);
+    }
+    if (matched) {
+      return;
+    }
+    throw error("Expected: " + (stringify(regexp)) + "\nto match: " + (red(abbreviate('string', string))), explanation);
+  },
+  throws: function(fn) {
+    var err, error1, explanation, name, negated, ref2;
+    ref2 = handleArgs(this, [1, 2], arguments, 'throws'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 2) {
+      explanation = arguments[0], fn = arguments[1];
+    }
+    if (typeof explanation === 'function') {
+      fn = explanation;
+      explanation = void 0;
+    }
+    if (typeof fn !== 'function') {
+      throw error(name + " expects " + (green('a function')) + " but got " + (red(fn)));
+    }
+    try {
+      fn();
+    } catch (error1) {
+      err = error1;
+      if (negated) {
+        throw error("Threw an exception despite " + name + " assertion:\n" + err.message, explanation);
+      }
+      return err;
+    }
+    if (negated) {
+      return;
+    }
+    throw error("Didn't throw an exception as expected to", explanation);
+  },
+  hasType: function(expectedType, value) {
+    var badArg, explanation, message, name, negated, ref2, stringType, suggestions, toBeOrNotToBe;
+    ref2 = handleArgs(this, [2, 3], arguments, 'hasType'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 3) {
+      explanation = arguments[0], expectedType = arguments[1], value = arguments[2];
+    }
+    stringType = getNameOfType(expectedType);
+    if (indexOf.call(types, stringType) < 0) {
+      badArg = stringify(expectedType);
+      suggestions = implodeNicely(types, 'or');
+      throw new TypeError(name + ": unknown expectedType " + badArg + "; you used:\n" + name + " " + (red(badArg)) + ", " + (stringify(value)) + "\nDid you mean " + suggestions + "?");
+    }
+    if (!(stringType === getTypeName(value) ^ negated)) {
+      value = red(stringify(value));
+      toBeOrNotToBe = (negated ? 'not ' : '') + 'to be';
+      message = "Expected value " + value + " " + toBeOrNotToBe + " of type " + stringType;
+      throw error(message, explanation);
+    }
+  }
+};
+
+nameNegative = function(name) {
+  if (name === 'truthy') {
+    return 'falsey';
+  }
+  if (name === 'resolves') {
+    return 'rejects';
+  }
+  return 'not' + name.charAt().toUpperCase() + name.slice(1);
+};
+
+positiveAssertions = ['truthy', 'equal', 'deepEqual', 'include', 'match', 'throws', 'hasType'];
+
+for (i = 0, len = positiveAssertions.length; i < len; i++) {
+  name = positiveAssertions[i];
+  assertSync[nameNegative(name)] = (function(name) {
+    return function() {
+      return assertSync[name].apply('!', arguments);
+    };
+  })(name);
+}
+
+assert = {
+  resolves: function(testee) {
+    var explanation, negated, ref2;
+    ref2 = handleArgs(this, [1, 2], arguments, 'resolves'), name = ref2[0], negated = ref2[1];
+    if (arguments.length === 2) {
+      explanation = arguments[0], testee = arguments[1];
+    }
+    if (!isPromiseAlike(testee)) {
+      throw error(name + " expects " + (green('a promise')) + " but got " + (red(stringify(testee))));
+    }
+    if (name === 'rejects') {
+      return testee.then((function() {
+        throw error("Promise wasn't rejected as expected to", explanation);
+      }), function(err) {
+        return err;
+      });
+    } else {
+      return testee["catch"](function(err) {
+        var ref3;
+        throw error("Promise was rejected despite resolves assertion:\n" + ((ref3 = err != null ? err.message : void 0) != null ? ref3 : err), explanation);
+      });
+    }
+  },
+  rejects: function(testee) {
+    return assert.resolves.apply('!', arguments);
+  }
+};
+
+fn1 = function(name, fn) {
+  return assert[name] = function() {
+    var args, testee;
+    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+    if (!args.length) {
+      return fn();
+    }
+    testee = args.pop();
+    if (isPromiseAlike(testee)) {
+      return testee.then(function(val) {
+        return fn.apply(null, slice.call(args).concat([val]));
+      });
+    } else {
+      return fn.apply(null, slice.call(args).concat([testee]));
+    }
+  };
+};
+for (name in assertSync) {
+  if (!hasProp.call(assertSync, name)) continue;
+  fn = assertSync[name];
+  fn1(name, fn);
+}
+
+types = ['null', 'Date', 'Array', 'String', 'RegExp', 'Boolean', 'Function', 'Object', 'NaN', 'Number', 'undefined'];
+
+isType = function(value, typeName) {
+  if (typeName === 'Date') {
+    return _.isDate(value) && !_.isNaN(+value);
+  }
+  return _['is' + typeName.charAt(0).toUpperCase() + typeName.slice(1)](value);
+};
+
+getTypeName = function(value) {
+  return _.find(types, _.partial(isType, value));
+};
+
+getNameOfType = function(x) {
+  switch (false) {
+    case !(x == null):
+      return "" + x;
+    case !isString(x):
+      return x;
+    case !isFunction(x):
+      return x.name;
+    case !_.isNaN(x):
+      return 'NaN';
+    default:
+      return x;
+  }
+};
+
+green = function(x) {
+  return "\x1B[32m" + x + "\x1B[39m";
+};
+
+red = function(x) {
+  return "\x1B[31m" + x + "\x1B[39m";
+};
+
+clear = '\x1b[39;49;00m';
+
+if (!(typeof process !== "undefined" && process !== null ? (ref2 = process.stdout) != null ? ref2.isTTY : void 0 : void 0)) {
+  green = red = function(x) {
+    return "" + x;
+  };
+  clear = '';
+}
+
+implodeNicely = function(list, conjunction) {
+  var first, last;
+  if (conjunction == null) {
+    conjunction = 'and';
+  }
+  first = list.slice(0, -1).join(', ');
+  last = list[list.length - 1];
+  return first + " " + conjunction + " " + last;
+};
+
+abbreviate = function(name, value, threshold) {
+  var desc, str;
+  if (threshold == null) {
+    threshold = 1024;
+  }
+  if ((str = stringify(value)).length <= threshold) {
+    return str;
+  }
+  desc = "length: " + value.length;
+  if (isArray(value)) {
+    desc += "; " + str.length + " JSON encoded";
+  }
+  if (name) {
+    name += ' ';
+  }
+  return "" + name + (type(value)) + "[" + desc + "]";
+};
+
+type = function(x) {
+  if (isString(x)) {
+    return 'String';
+  }
+  if (isNumber(x)) {
+    return 'Number';
+  }
+  if (isRegExp(x)) {
+    return 'RegExp';
+  }
+  if (isArray(x)) {
+    return 'Array';
+  }
+  throw new TypeError("unsupported type: " + x);
+};
+
+asRegExp = function(re) {
+  var flags;
+  flags = '';
+  if (re.global) {
+    flags += 'g';
+  }
+  if (re.multiline) {
+    flags += 'm';
+  }
+  if (re.ignoreCase) {
+    flags += 'i';
+  }
+  return "/" + re.source + "/" + flags;
+};
+
+toString = Object.prototype.toString;
+
+stringify = function(x) {
+  var className, e, error1, json;
+  if (x == null) {
+    return "" + x;
+  }
+  if (_.isNaN(x)) {
+    return 'NaN';
+  }
+  if (isRegExp(x)) {
+    return asRegExp(x);
+  }
+  if (typeof x === 'symbol') {
+    return x.toString();
+  }
+  json = JSON.stringify(x, function(key, val) {
+    if (typeof val === 'function') {
+      return toString(val);
+    }
+    if (isRegExp(val)) {
+      return asRegExp(val);
+    }
+    return val;
+  });
+  if (typeof x !== 'object' || includes(['Object', 'Array'], className = x.constructor.name)) {
+    return json;
+  }
+  if (x instanceof Error || /Error/.test(className)) {
+    if (json === '{}') {
+      return x.stack;
+    }
+    return x.stack + "\nwith error metadata:\n" + json;
+  }
+  if (x.toString === toString) {
+    return className;
+  }
+  try {
+    return className + "[" + x + "]";
+  } catch (error1) {
+    e = error1;
+    return className;
+  }
+};
+
+error = function(message, explanation) {
+  if (explanation != null) {
+    message = "Assertion failed: " + explanation + "\n" + clear + message;
+  }
+  return new Error(message);
+};
+
+handleArgs = function(self, count, args, name, help) {
+  var actual, actualArgs, argc, functionSource, max, message, n, negated, wanted, wantedArgCount, wantedArgNames;
+  negated = false;
+  if (isString(self)) {
+    negated = true;
+    name = nameNegative(name);
+  }
+  argc = args.length;
+  if (argc === count) {
+    return [name, negated];
+  }
+  max = '';
+  if (isArray(count) && indexOf.call(count, argc) >= 0) {
+    n = count[count.length - 1];
+    if ((argc !== n) || isString(args[0])) {
+      return [name, negated];
+    }
+    max = ",\nand when called with " + n + " args, the first arg must be a docstring";
+  }
+  if (isNumber(count)) {
+    wantedArgCount = count + " argument";
+  } else {
+    wantedArgCount = count.slice(0, -1).join(', ');
+    count = count.pop();
+    wantedArgCount = wantedArgCount + " or " + count + " argument";
+  }
+  if (count !== 1) {
+    wantedArgCount += 's';
+  }
+  actualArgs = stringify([].slice.call(args)).slice(1, -1);
+  functionSource = Function.prototype.toString.call(assert[name]);
+  wantedArgNames = functionSource.match(/^function\s*[^(]*\s*\(([^)]*)/)[1];
+  if (max) {
+    wantedArgNames = "explanation, " + wantedArgNames;
+  }
+  wanted = name + "(" + wantedArgNames + ")";
+  actual = name + "(" + actualArgs + ")";
+  message = (green(wanted)) + " needs " + (wantedArgCount + max) + "\nyour usage: " + (red(actual));
+  if (typeof help === 'function') {
+    help = help();
+  }
+  throw error(message, help);
+};
+
+isPromiseAlike = function(p) {
+  return p === Object(p) && 'function' === typeof p.then;
+};
+
+if (((typeof module !== "undefined" && module !== null ? module.exports : void 0) != null)) {
+  module.exports = assert;
+} else {
+  global.assert = assert;
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..64998b1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,61 @@
+{
+  "name": "assertive",
+  "version": "2.1.1",
+  "description": "Assertive is a terse yet expressive assertion library, designed and ideally suited for coffee-script",
+  "license": "BSD-3-Clause",
+  "main": "lib/assertive.js",
+  "homepage": "https://github.com/groupon/assertive",
+  "repository": {
+    "type": "git",
+    "url": "git+ssh://git@github.com/groupon/assertive"
+  },
+  "bugs": {
+    "url": "https://github.com/groupon/assertive/issues"
+  },
+  "scripts": {
+    "build": "rm -rf lib && coffee --no-header -cbo lib src",
+    "pretest": "npm run build",
+    "test": "mocha",
+    "posttest": "nlm verify",
+    "test-run": "coffeelint src test && mocha",
+    "watch": "coffee --no-header -wcbo lib src & nodemon -w lib -w test -e coffee,js,json -x \"mocha\""
+  },
+  "engines": {
+    "node": ">=0.8"
+  },
+  "nlm": {
+    "license": {
+      "files": [
+        "src"
+      ]
+    }
+  },
+  "dependencies": {
+    "lodash": "^4.6.1"
+  },
+  "devDependencies": {
+    "bluebird": "^3.3.4",
+    "coffee-script": "^1.10.0",
+    "coffeelint": "^1.10.1",
+    "coffeelint-use-strict": "0.0.1",
+    "mocha": "^2.0.0",
+    "nlm": "^2.0.0",
+    "nodemon": "^1.0.0"
+  },
+  "author": {
+    "name": "Groupon",
+    "email": "opensource at groupon.com"
+  },
+  "contributors": [
+    "David Bushong <dbushong at groupon.com>",
+    "Johan Sundström <jsundstrom at groupon.com>",
+    "Sean Massa <smassa at groupon.com>"
+  ],
+  "files": [
+    "*.js",
+    "lib"
+  ],
+  "publishConfig": {
+    "registry": "https://registry.npmjs.org"
+  }
+}
diff --git a/src/assertive.coffee b/src/assertive.coffee
new file mode 100644
index 0000000..4f350d5
--- /dev/null
+++ b/src/assertive.coffee
@@ -0,0 +1,390 @@
+'use strict'
+
+###
+Copyright (c) 2013, Groupon, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+Neither the name of GROUPON nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+###
+
+# eat _ off the global scope, or require it ourselves if missing
+
+global = Function('return this')()
+{includes, isEqual, isString, isNumber, isRegExp, isArray, isFunction, map} =
+_ = global._ ? require 'lodash'
+
+
+assertSync =
+  truthy: (bool) ->
+    [name, negated] = handleArgs this, [1, 2], arguments, 'truthy'
+    [explanation, bool] = arguments  if arguments.length is 2
+    unless (!!bool) ^ negated
+      throw error "Expected #{red stringify bool} to be #{name}", explanation
+
+  expect: (bool) ->
+    [explanation, bool] = arguments  if arguments.length is 2
+    if explanation
+      assertSync.equal explanation, true, bool
+    else
+      assertSync.equal true, bool
+
+  equal: (expected, actual) ->
+    [name, negated] = handleArgs this, [2, 3], arguments, 'equal'
+    [explanation, expected, actual] = arguments  if arguments.length is 3
+    if negated
+      if expected is actual
+        throw error 'notEqual assertion expected ' + red(stringify actual) +
+        ' to be exactly anything else', explanation
+    else if expected isnt actual
+      throw error """Expected: #{green stringify expected}
+                     Actually: #{red stringify actual}""", explanation
+
+  deepEqual: (expected, actual) ->
+    [name, negated] = handleArgs this, [2, 3], arguments, 'deepEqual'
+    [explanation, expected, actual] = arguments  if arguments.length is 3
+    unless isEqual(expected, actual) ^ negated
+      wrongLooks = stringify actual
+      if negated
+        throw error """notDeepEqual assertion expected exactly anything else but
+                       #{red(wrongLooks)}""", explanation
+
+      rightLooks = stringify expected
+      message = if wrongLooks is rightLooks
+        """deepEqual #{green rightLooks} failed on something that
+           serializes to the same result (likely some function)"""
+      else
+        """Expected: #{green rightLooks}
+           Actually: #{red wrongLooks}"""
+      throw error message, explanation
+
+  include: (needle, haystack) ->
+    [name, negated] = handleArgs this, [2, 3], arguments, 'include'
+    [explanation, needle, haystack] = arguments  if arguments.length is 3
+    if isString haystack
+      if needle is ''
+        what = if negated then 'always-failing test' else 'no-op test'
+        throw error "#{what} detected: all strings contain the empty string!"
+      unless isString(needle) or isNumber(needle) or isRegExp(needle)
+        problem = 'needs a RegExp/String/Number needle for a String haystack'
+        throw new TypeError """#{name} #{problem}; you used:
+          #{name} #{green stringify haystack}, #{red stringify needle}"""
+    else unless isArray haystack
+      needle = stringify needle
+      throw new TypeError """#{name} takes a String or Array haystack; you used:
+                             #{name} #{red stringify haystack}, #{needle}"""
+
+    if isString haystack
+      if isRegExp needle
+        contained = haystack.match needle
+      else
+        contained = haystack.indexOf(needle) isnt -1
+    else
+      contained = includes haystack, needle
+
+    verb = if isRegExp needle then 'match' else 'include'
+    if negated
+      if contained
+        message = """notInclude expected needle not to be found in haystack
+                     - needle: #{stringify needle}
+                     haystack: #{abbreviate '', haystack}"""
+        if isString(haystack) and isRegExp(needle)
+          message += ', but found:\n'
+          if needle.global
+            message += contained.map((s) -> "* #{red stringify s}").join '\n'
+          else
+            message += "* #{red stringify contained[0]}"
+        throw error message, explanation
+    else if not contained
+      throw error """#{name} expected needle to be found in haystack
+                     - needle: #{stringify needle}
+                     haystack: #{abbreviate '', haystack}""", explanation
+
+  match: (regexp, string) ->
+    [name, negated] = handleArgs this, [2, 3], arguments, 'match'
+    [explanation, regexp, string] = arguments  if arguments.length is 3
+    unless (re = isRegExp regexp) and isString string
+      string = abbreviate 'string', string
+      called = "#{name} #{stringify regexp}, #{red string}"
+      if not re
+        oops = 'regexp arg is not a RegExp'
+      else
+        oops = 'string arg is not a String'
+      throw new TypeError "#{name}: #{oops}; you used:\n#{called}"
+
+    matched = regexp.test string
+    if negated
+      return  unless matched
+      message = """Expected: #{stringify regexp}
+                   not to match: #{red abbreviate 'string', string}"""
+      if regexp.global
+        count = string.match(regexp).length
+        message += "\nMatches: #{red count}"
+      throw error message, explanation
+    return  if matched
+    throw error """Expected: #{stringify regexp}
+                   to match: #{red abbreviate 'string', string}""", explanation
+
+
+  throws: (fn) ->
+    [name, negated] = handleArgs this, [1, 2], arguments, 'throws'
+    [explanation, fn] = arguments  if arguments.length is 2
+    if typeof explanation is 'function'
+      fn = explanation
+      explanation = undefined
+    unless typeof fn is 'function'
+      throw error "#{name} expects #{green 'a function'} but got #{red fn}"
+
+    try
+      fn()
+    catch err
+      if negated
+        throw error """Threw an exception despite #{name} assertion:
+                       #{err.message}""", explanation
+      return err
+
+    return if negated
+    throw error "Didn't throw an exception as expected to", explanation
+
+  hasType: (expectedType, value) ->
+    [name, negated] = handleArgs this, [2, 3], arguments, 'hasType'
+    [explanation, expectedType, value] = arguments  if arguments.length is 3
+
+    stringType = getNameOfType expectedType
+    unless stringType in types
+      badArg = stringify expectedType
+      suggestions = implodeNicely types, 'or'
+      throw new TypeError """#{name}: unknown expectedType #{badArg}; you used:
+                             #{name} #{red badArg}, #{stringify value}
+                             Did you mean #{suggestions}?"""
+
+    unless stringType is getTypeName(value) ^ negated
+      value = red stringify value
+      toBeOrNotToBe = (if negated then 'not ' else '') + 'to be'
+      message = "Expected value #{value} #{toBeOrNotToBe} of type #{stringType}"
+      throw error message, explanation
+
+
+nameNegative = (name) ->
+  return 'falsey'  if name is 'truthy'
+  return 'rejects' if name is 'resolves'
+  'not' + name.charAt().toUpperCase() + name.slice 1
+
+# produce negatived versions of all the common assertion functions
+positiveAssertions = [
+  'truthy'
+  'equal'
+  'deepEqual'
+  'include'
+  'match'
+  'throws'
+  'hasType'
+]
+for name in positiveAssertions
+  assertSync[nameNegative name] = do (name) -> ->
+    assertSync[name].apply '!', arguments
+
+# promise-specific tests
+assert =
+  resolves: (testee) ->
+    [name, negated] = handleArgs this, [1, 2], arguments, 'resolves'
+    [explanation, testee] = arguments if arguments.length is 2
+
+    unless isPromiseAlike testee
+      throw error(
+        "#{name} expects #{green 'a promise'} but got #{red stringify testee}"
+      )
+
+    if name is 'rejects'
+      testee.then(
+        (-> throw error "Promise wasn't rejected as expected to", explanation),
+        (err) -> err
+      )
+    else
+      testee.catch (err) ->
+        throw error """Promise was rejected despite resolves assertion:
+                       #{err?.message ? err}""", explanation
+
+  rejects: (testee) -> assert.resolves.apply '!', arguments
+
+# union of promise-specific and promise-aware wrapped synchronous tests
+for own name, fn of assertSync
+  do (name, fn) ->
+    assert[name] = (args...) ->
+      return fn() unless args.length
+      testee = args.pop()
+      if isPromiseAlike testee
+        testee.then (val) -> fn args..., val
+      else
+        fn args..., testee
+
+# listing the most specific types first lets us iterate in order and verify that
+# the expected type was the first match
+types = [
+  'null'
+  'Date'
+  'Array'
+  'String'
+  'RegExp'
+  'Boolean'
+  'Function'
+  'Object'
+  'NaN'
+  'Number'
+  'undefined'
+]
+
+isType = (value, typeName) ->
+  return _.isDate(value) and not _.isNaN(+value)  if typeName is 'Date'
+  _['is' + typeName.charAt(0).toUpperCase() + typeName.slice(1)] value
+
+# gets the name of the type that value is an incarnation of
+getTypeName = (value) ->
+  _.find types, _.partial isType, value
+
+# translates any argument we were meant to interpret as a type, into its name
+getNameOfType = (x) ->
+  switch
+    when not x?       then "#{x}" # null / undefined
+    when isString x   then x
+    when isFunction x then x.name
+    when _.isNaN x    then 'NaN'
+    else x
+
+green = (x) -> "\x1B[32m#{ x }\x1B[39m"
+red = (x) -> "\x1B[31m#{ x }\x1B[39m"
+clear = '\x1b[39;49;00m'
+
+unless process?.stdout?.isTTY
+  green = red = (x) -> "#{x}"
+  clear = ''
+
+implodeNicely = (list, conjunction = 'and') ->
+  first = list.slice(0, -1).join(', ')
+  last = list[list.length - 1]
+  "#{first} #{conjunction} #{last}"
+
+abbreviate = (name, value, threshold = 1024) ->
+  return str  if (str = stringify value).length <= threshold
+  desc = "length: #{value.length}"
+  desc += "; #{str.length} JSON encoded"  if isArray value
+  name += ' '  if name
+  "#{name}#{type value}[#{desc}]"
+
+type = (x) ->
+  return 'String' if isString x
+  return 'Number' if isNumber x
+  return 'RegExp' if isRegExp x
+  return 'Array'  if isArray  x
+  throw new TypeError "unsupported type: #{x}"
+
+asRegExp = (re) ->
+  flags = ''
+  flags += 'g'  if re.global
+  flags += 'm'  if re.multiline
+  flags += 'i'  if re.ignoreCase
+  "/#{re.source}/#{flags}"
+
+toString = Object::toString
+
+stringify = (x) ->
+  return "#{x}"  unless x?
+  return 'NaN'  if _.isNaN x
+  return asRegExp x  if isRegExp x
+  return x.toString()  if typeof x is 'symbol'
+  json = JSON.stringify x, (key, val) ->
+    return toString val  if typeof val is 'function'
+    return asRegExp val  if isRegExp val
+    return val
+  if typeof x isnt 'object' \
+  or includes ['Object', 'Array'], className = x.constructor.name
+    return json
+
+  if x instanceof Error or /Error/.test className
+    return x.stack  if json is '{}'
+    return "#{x.stack}\nwith error metadata:\n#{json}"
+  return className  if x.toString is toString
+  try
+    return "#{className}[#{x}]"
+  catch e
+    return className
+
+error = (message, explanation) ->
+  if explanation?
+    message = "Assertion failed: #{explanation}\n#{clear}#{message}"
+  new Error message
+
+# assert that the function got `count` args (if an integer), one of the number
+# of args (if an array of legal counts), and if it was an array and the count
+# was equal to the last option (fully populated), that the first arg is a String
+# (that test's semantic explanation)
+handleArgs = (self, count, args, name, help) ->
+  negated = false
+  if isString self
+    negated = true
+    name = nameNegative name
+
+  argc = args.length
+  return [name, negated]  if argc is count
+
+  max = ''
+  if isArray(count) and argc in count
+    n = count[count.length - 1]
+    return [name, negated]  if (argc isnt n) or isString args[0]
+    max = """,
+      and when called with #{n} args, the first arg must be a docstring"""
+
+  if isNumber count
+    wantedArgCount = "#{count} argument"
+  else
+    wantedArgCount = count.slice(0, -1).join(', ')
+    count = count.pop()
+    wantedArgCount = "#{wantedArgCount} or #{count} argument"
+  wantedArgCount += 's' unless count is 1
+
+  actualArgs = stringify([].slice.call args).slice 1, -1
+
+  functionSource = Function::toString.call assert[name]
+  wantedArgNames = functionSource.match(/^function\s*[^(]*\s*\(([^)]*)/)[1]
+  wantedArgNames = "explanation, #{wantedArgNames}"  if max
+
+  wanted = "#{name}(#{wantedArgNames})"
+  actual = "#{name}(#{actualArgs})"
+  message = """#{green wanted} needs #{wantedArgCount + max}
+    your usage: #{red actual}"""
+
+  help = help()  if typeof help is 'function'
+  throw error message, help
+
+# borrowed from Q
+isPromiseAlike = (p) -> p is Object(p) and 'function' is typeof p.then
+
+# export as a module to node - or to the global scope, if not
+if (module?.exports?)
+  module.exports = assert
+else
+  global.assert = assert
diff --git a/test/assertive.test.coffee b/test/assertive.test.coffee
new file mode 100644
index 0000000..17d9047
--- /dev/null
+++ b/test/assertive.test.coffee
@@ -0,0 +1,661 @@
+'use strict'
+
+Promise = require 'bluebird'
+
+{ truthy,    falsey
+, expect,    notExpect
+, equal,     notEqual
+, deepEqual, notDeepEqual
+, include,   notInclude
+, match,     notMatch
+, throws,    notThrows
+, hasType,   notHasType
+, resolves,  rejects
+} = require '../lib/assertive'
+
+describe 'throws', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> throws()
+    throws -> throws(1, 2, 3)
+
+  it 'notes that arg 1 must be a string when called with 2 args', ->
+    err = throws -> throws 1, 'description in wrong arg'
+    include 'called with 2 args, the first arg must be a docstring', err.message
+
+  it 'errors out unless you pass a function to execute', ->
+    throws -> throws(68000)
+    throws -> throws('more awesomeness', 68020)
+
+  it 'returns the exception that was thrown by the provided function', ->
+    exception = new Error 68040
+    equal throws(-> throw exception), exception
+    equal throws(-> throw 'we suck'), 'we suck'
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'No error was thrown - this is a problem'
+    err = throws -> throws explanation, ->
+    include explanation, err.message
+
+
+describe 'notThrows', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> notThrows()
+    throws -> notThrows(1, 2, 3)
+
+  it 'notes that arg 1 must be a string when called with 2 args', ->
+    err = throws -> notThrows 1, 'description in wrong arg'
+    include 'called with 2 args, the first arg must be a docstring', err.message
+
+  it 'errors out unless you pass a function to execute', ->
+    throws -> notThrows(68000)
+    throws -> notThrows('more awesomeness', 68020)
+
+  it 'captures and shows exceptions thrown by the provided function', ->
+    thrown = new Error 68040
+    caught = throws -> notThrows -> throw thrown
+    truthy 'the exception thrown was caught and a new exception thrown', caught
+    include 'Threw an exception despite notThrows assertion:', caught.message
+    include 'assertion:\n68040', caught.message
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'No error was thrown - this is a problem'
+    err = throws -> notThrows explanation, -> throw new Error 'aiee!'
+    include explanation, err.message
+
+nonTrueOutcome = (fn, outcome) ->
+  ->
+    outcome -> fn 1
+    outcome -> fn {}
+    outcome -> fn []
+    outcome -> fn '!'
+    outcome -> fn 0
+    outcome -> fn ''
+    outcome -> fn null
+    outcome -> fn false
+    outcome -> fn undefined
+
+truthyOutcome = (fn, outcome) ->
+  ->
+    outcome -> fn 1
+    outcome -> fn {}
+    outcome -> fn []
+    outcome -> fn '!'
+    outcome -> fn true
+
+falseyOutcome = (fn, outcome) ->
+  ->
+    outcome -> fn 0
+    outcome -> fn ''
+    outcome -> fn null
+    outcome -> fn false
+    outcome -> fn undefined
+
+truthyIsNoOp = (fn) -> truthyOutcome fn, notThrows
+falseyIsNoOp = (fn) -> falseyOutcome fn, notThrows
+truthyThrows = (fn) -> truthyOutcome fn, throws
+falseyThrows = (fn) -> falseyOutcome fn, throws
+nonTrueThrows = (fn) -> nonTrueOutcome fn, throws
+
+describe 'truthy', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> truthy()
+    throws -> truthy('description', true, 3)
+
+  it 'notes that arg 1 must be a string when called with 2 args', ->
+    err = throws -> truthy true, 'description in wrong arg'
+    include 'called with 2 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed a truthy value", -> truthyIsNoOp truthy
+
+  it 'errors out when passed a falsey value', falseyThrows truthy
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> truthy explanation, false
+    include explanation, err.message
+
+
+describe 'falsey', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> falsey()
+    throws -> falsey('description', false, 3)
+
+  it 'notes that arg 1 must be a string when called with 2 args', ->
+    err = throws -> falsey false, 'description in wrong arg'
+    include 'called with 2 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed a falsey value", falseyIsNoOp falsey
+
+  it 'errors out when passed a truthy value', truthyThrows falsey
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'falsey will confidently reject all your truths'
+    err = throws -> falsey explanation, true
+    include explanation, err.message
+
+
+describe 'expect', ->
+  it 'errors out when you provide too few or way too many args', ->
+    throws -> expect()
+    throws -> expect('desc', 1, 2)
+
+  it "doesn't do anything when passed true", ->
+    expect 'It really is true', true
+    expect 2 > 1
+
+  it 'errors out when passed anything but true', nonTrueThrows expect
+
+  it 'is not very likely to accept mistakes', ->
+    err = throws -> expect 'te' + 'st', 'test' # people try this kind of stuff
+    include /Expected: .*true/, err.message
+    include /Actually: .*"test"/, err.message
+
+
+describe 'equal', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> equal()
+    throws -> equal(1)
+    throws -> equal('description', 1, 1, 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    err = throws -> equal 1, 1, 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed two identical values", ->
+    equal 1, 1
+    equal this, this
+    equal null, null
+    equal undefined, undefined
+
+  it 'errors out when passed two non-identical values', ->
+    throws -> equal 0, 1
+    throws -> equal {}, {}
+    throws -> equal null, undefined
+    throws -> equal false, 0
+    throws -> equal [], []
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> equal explanation, 0, 1
+    include explanation, err.message
+
+  if typeof Symbol != 'undefined'
+    it 'nicely formats non-matching symbols', ->
+      err = throws -> equal Symbol('some'), Symbol('other')
+      include 'Symbol(some)', err.message
+      include 'Symbol(other)', err.message
+
+
+describe 'notEqual', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> notEqual()
+    throws -> notEqual(1)
+    throws -> notEqual('description', 1, 2, 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    err = throws -> notEqual 1, 2, 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed two different values", ->
+    notEqual 1, 2
+    notEqual this, Object.create this
+    notEqual NaN, NaN
+    notEqual null, undefined
+
+  it 'errors out when passed two identical values', ->
+    throws -> notEqual 0, 0
+    throws -> notEqual this, this
+    throws -> notEqual null, null
+    throws -> notEqual (x = []), x
+    throws -> notEqual undefined, undefined
+    throws -> notEqual false, false
+    throws -> notEqual true, true
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = "it doesn't get more equal than identical"
+    err = throws -> notEqual explanation, Math.PI, Math.PI
+    include explanation, err.message
+
+
+describe 'deepEqual', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> deepEqual()
+    throws -> deepEqual(1)
+    throws -> deepEqual('description', 1, 1, 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    err = throws -> deepEqual 1, 1, 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed two deepEqual values", ->
+    deepEqual 1, 1
+    deepEqual this, this
+    deepEqual null, null
+    deepEqual undefined, undefined
+    deepEqual [1], [1]
+    deepEqual { a: this }, { a: this }
+    deepEqual [null], [null]
+    deepEqual { u: undefined }, { u: undefined }
+
+  it 'errors out when passed two non-deepEqual values', ->
+    throws -> deepEqual {}, []
+    throws -> deepEqual 0, 1
+    throws -> deepEqual null, undefined
+    throws -> deepEqual false, 0
+    throws -> deepEqual [1], [2]
+    throws -> deepEqual { a: {} }, { a: [] }
+    throws -> deepEqual [null], [undefined]
+    throws -> deepEqual {}, { u: undefined }
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> deepEqual explanation, [null], [undefined]
+    include explanation, err.message
+
+
+describe 'notDeepEqual', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> notDeepEqual()
+    throws -> notDeepEqual(1)
+    throws -> notDeepEqual('description', 1, 2, 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    err = throws -> notDeepEqual 1, 2, 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', err.message
+
+  it "doesn't do anything when passed two not deepEqual values", ->
+    notDeepEqual 1, 2
+    notDeepEqual 2, []
+    notDeepEqual this, {}
+    notDeepEqual null, undefined
+    notDeepEqual String, Number
+    notDeepEqual [1], [2]
+    notDeepEqual { a: this }, { b: this }
+    notDeepEqual [null], [undefined]
+    notDeepEqual {}, { u: undefined }
+
+  it 'errors out when passed two deepEqual values', ->
+    throws -> notDeepEqual {}, {}
+    throws -> notDeepEqual 0, 0
+    throws -> notDeepEqual null, null
+    throws -> notDeepEqual false, false
+    throws -> notDeepEqual [1], [1]
+    throws -> notDeepEqual { a: {} }, { a: {} }
+    throws -> notDeepEqual [null], [null]
+    throws -> notDeepEqual [[{}]], [[{}]]
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> notDeepEqual explanation, [null], [null]
+    include explanation, err.message
+
+
+describe 'include', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> include()
+    throws -> include([1])
+    throws -> include('description', 1, [1], 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    err = throws -> include 1, [1], 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', err.message
+
+  it 'throws TypeErrors on bad needles', ->
+    saneNeedlesPlease = /needs a .* needle for a String haystack/
+    e = throws -> include undefined, 'undefined? baad'
+    match saneNeedlesPlease, e.message
+    e = throws -> include (->), 'function? worse'
+    match saneNeedlesPlease, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'throws an error for the no-op assertion string-contains-empty-string', ->
+    err = throws -> include '', 'all strings include the empty string'
+    expect = 'no-op test detected: all strings contain the empty string!'
+    include expect, err.message
+
+  it 'raises a helpful TypeError when you pass a bad haystack', ->
+    err = throws -> include 0, 1000
+    include 'include takes a String or Array haystack', err.message
+    truthy 'include throws a TypeError on bad args', err instanceof TypeError
+
+  it "doesn't do anything when passed an array including the value", ->
+    include 1, [1]
+    include this, [1, this, 3]
+    include null, [null, 2]
+    include undefined, [1, undefined]
+
+  it "doesn't do anything when passed a string including the value", ->
+    include 'ho', 'hey ho'
+    include 'ho', 'hoopla'
+    include 'ho', 'what ho, Jeeves?'
+
+  it "doesn't do anything when passed a string matching the RegExp", ->
+    include /h[oe]/, 'hey ho'
+    include /^ho+pla$/, 'hoopla'
+    include /jeeves/i, 'what ho, Jeeves?'
+    include /^$/, ''
+
+  it "errors out when the string passed doesn't include the needle", ->
+    throws -> include '/spelunking/', 'spelunking'
+    throws -> include 'SAY, WHAT?', 'say, what?'
+
+  it 'errors out the same way when testing for a needle in an empty array', ->
+    err = throws -> include 'not present in array', []
+    include '''include expected needle to be found in haystack
+               - needle: "not present in array"
+               haystack: []''', err.message
+
+  it 'errors out the same way when testing for a needle in an empty string', ->
+    err = throws -> include 'not present in string', ''
+    include '''include expected needle to be found in haystack
+               - needle: "not present in string"
+               haystack: ""''', err.message
+
+  it "errors out when the string passed doesn't match the RegExp", ->
+    throws -> include /SPELUNKING/, 'spelunking'
+    throws -> include /SAY, WHAT$/i, 'say, what?'
+
+  it "errors out when the array passed doesn't include the value", ->
+    throws -> include [], [{}]
+    throws -> include 1, [0]
+    throws -> include undefined, [null]
+    throws -> include null, [undefined]
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> include explanation, [undefined], [null]
+    include explanation, err.message
+
+  it 'shortens larger haystacks in the assertion message', ->
+    err = throws -> include 2001, [1..2000]
+    match /^include [\s\S]*haystack: Array\[length: 2000; \d* JSON/, err.message
+
+
+describe 'notInclude', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> notInclude()
+    throws -> notInclude(2)
+    throws -> notInclude('description', 2, [1], 4)
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    e = throws -> notInclude 2, [1], 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', e.message
+
+  it 'throws TypeErrors on bad needles', ->
+    saneNeedlesPlease = /needs a .* needle for a String haystack/
+    e = throws -> notInclude undefined, 'undefined? baad'
+    match saneNeedlesPlease, e.message
+    e = throws -> notInclude (->), 'function? worse'
+    match saneNeedlesPlease, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'raises a helpful TypeError when you pass a bad haystack', ->
+    e = throws -> notInclude 0, 1000
+    include 'notInclude takes a String or Array haystack', e.message
+    truthy 'notInclude throws a TypeError on bad args', e instanceof TypeError
+
+  it 'throws an error for the bad assertion string-lacks-empty-string', ->
+    err = throws -> notInclude '', 'all strings include the empty string'
+    want = 'always-failing test detected: all strings contain the empty string!'
+    include want, err.message
+
+  it 'lets you pass an empty haystack if you want to', ->
+    notThrows -> notInclude 'Your expectations are just fine, love', []
+
+  it "doesn't do anything when passed an array not including the value", ->
+    notInclude 2, [1]
+    notInclude Object.create(this), [1, this, 3]
+    notInclude 1, [null, 2]
+    notInclude null, [1, undefined]
+
+  it 'errors out when passed a string including the value', ->
+    throws -> notInclude 'ho', 'hey ho'
+    throws -> notInclude 'ho', 'hoopla'
+    throws -> notInclude 'ho', 'what ho, Jeeves?'
+
+  it 'errors out when passed a string matching the RegExp', ->
+    throws -> notInclude /h[oe]/, 'hey ho'
+    throws -> notInclude /^ho+pla$/, 'hoopla'
+    throws -> notInclude /jeeves/i, 'what ho, Jeeves?'
+
+  it "doesn't do anything when the string passed doesn't include the needle", ->
+    notInclude '/spelunking/', 'spelunking'
+    notInclude 'SAY, WHAT?', 'say, what?'
+
+  it "doesn't do anything when the string passed doesn't match the RegExp", ->
+    notInclude /SPELUNKING/, 'spelunking'
+    notInclude /SAY, WHAT$/i, 'say, what?'
+
+  it 'errors out when the array passed does include the value', ->
+    throws -> notInclude 0, [0]
+    throws -> notInclude this, [this]
+    throws -> notInclude null, [null]
+    throws -> notInclude undefined, [undefined]
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    err = throws -> notInclude explanation, null, [null]
+    include explanation, err.message
+
+  it 'shortens larger haystacks in the assertion message', ->
+    err = throws -> notInclude 2000, [1..2000]
+    match /notInclude[\s\S]*stack: Array\[length: 2000; \d* JSON/, err.message
+
+
+describe 'match', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> match()
+    throws -> match(/foo/)
+    throws -> match('description', /foo/, 'foo', 'bar')
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    e = throws -> match /foo/, 'foo', 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', e.message
+
+  it 'throws TypeErrors when called with a non-RegExp', ->
+    e = throws -> match 'foo', 'foobar'
+    match /regexp arg is not a RegExp; you used:\nmatch .*"foo"/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'throws TypeErrors when called with a non-String', ->
+    e = throws -> match /X/, undefined
+    match /string arg is not a String; you used:\nmatch .*X.*undefin/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it "doesn't do anything when passed a RegExp that the string matches", ->
+    match /^fo*/i, 'FOOBAR'
+
+  it "doesn't break when also passed a docstring", ->
+    match 'still fine and dandy', /^fo*/i, 'FOOBAR'
+
+  it 'errors out when the RegExp does not match the passed string', ->
+    e = throws -> match /wrong case/, 'WRONG CASE'
+    match /Expected: \/wrong case\/\nto match: .*"WRONG CASE"/, e.message
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    e = throws -> match explanation, /aye/, 'nay'
+    include explanation, e.message
+
+  it 'shortens larger strings in the assertion message', ->
+    e = throws -> match /4711/, JSON.stringify [1..2000]
+    include 'string String[length: 8894]', e.message
+
+
+describe 'notMatch', ->
+  it 'errors out when you provide too few or too many args', ->
+    throws -> notMatch()
+    throws -> notMatch(/foo/)
+    throws -> notMatch('description', /foo/, 'foo', 'bar')
+
+  it 'notes that arg 1 must be a string when called with 3 args', ->
+    e = throws -> notMatch /foo/, 'foo', 'description in wrong arg'
+    include 'called with 3 args, the first arg must be a docstring', e.message
+
+  it 'throws TypeErrors when called with a non-RegExp', ->
+    e = throws -> notMatch 'bar', 'barfoo'
+    match /regexp arg is not a RegExp; you used:\nnotMatch .*"bar"/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'throws TypeErrors when called with a non-String', ->
+    e = throws -> notMatch /x/, undefined
+    match /string arg is not a String; you used:\nnotMatch.*undefine/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it "doesn't do anything when passed a RegExp not matching the string", ->
+    notMatch /^not/, 'FOOBAR'
+
+  it "doesn't break when also passed a docstring", ->
+    notMatch 'still fine and dandy', /^not/, 'FOOBAR'
+
+  it 'errors out when the RegExp matches the passed string', ->
+    e = throws -> notMatch /problem/i, 'Problems found!'
+    match /Expected: \/problem\/i\nnot to match: .*"Problems found!"/, e.message
+
+  it 'shows how many matches we did find when passed a global RegExp', ->
+    e = throws -> notMatch /yo+/gi, 'Yo, yo, yo, yooo, man!'
+    match /Matches: .*4\b/, e.message
+
+  it 'includes your helpful explanation, when provided', ->
+    explanation = 'Given falsum, we can derive anything, which is awesome!'
+    e = throws -> notMatch explanation, /woo/, 'woo!'
+    include explanation, e.message
+
+  it 'shortens larger strings in the assertion message', ->
+    e = throws -> notMatch /200/, JSON.stringify [1..2000]
+    include 'string String[length: 8894]', e.message
+
+
+describe 'hasType', ->
+  it 'errors out when you provide too few, too many, or incorrect args', ->
+    throws -> hasType()
+    throws -> hasType String
+    throws -> hasType 42, 42
+    throws -> hasType 'explanation', String, 'some thing', 'extra'
+
+  it 'explains correct types for wrong ones', ->
+    e = throws -> hasType 42, 42
+    match /unknown expectedType/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'recognizes Strings', ->
+    hasType String, '42'
+    throws -> hasType String, 42
+
+  it 'recognizes Numbers', ->
+    hasType '42 is a Number', Number, 42
+    throws -> hasType Number, '42'
+
+  it 'recognizes NaN', ->
+    hasType NaN, NaN
+    throws 'Number tested as being NaN', -> hasType NaN, 68881
+
+  it 'recognizes RegExps', ->
+    hasType RegExp, /howdy/
+    throws -> hasType RegExp, '/howdy/'
+
+  it 'recognizes Arrays', ->
+    hasType Array, [1, 2, 3]
+    throws -> hasType Array, '[1, 2, 3]'
+
+  it 'recognizes Functions', ->
+    hasType Function, ->
+    throws -> hasType Function, 'function () { }'
+
+  it 'recognizes Objects', ->
+    hasType Object, foo: 42
+    throws 'Array tested as being an Object', -> hasType Object, [1, 2, 3]
+
+  it 'recognizes Dates', ->
+    hasType Date, new Date()
+    invalidDate = new Date 'Invalid Date'
+    throws 'Invalid Date tested as being a Date', -> hasType Date, invalidDate
+    throws 'Object tested as being a Date', -> hasType Date, getTime: -> 0
+
+  it 'recognizes null', ->
+    hasType null, null
+    throws 'Object tested as being null', -> hasType null, {}
+
+  it 'recognizes undefined', ->
+    hasType undefined, undefined
+    throws 'Object tested as being undefined', -> hasType undefined, {}
+
+
+describe 'notHasType', ->
+  it 'errors out when you provide too few, too many, or incorrect args', ->
+    throws -> notHasType()
+    throws -> notHasType String
+    throws -> notHasType 42, 42
+    throws -> notHasType 'explanation', String, 'some thing', 'extra'
+
+  it 'explains correct types for wrong ones', ->
+    e = throws -> notHasType 42, 42
+    match /unknown expectedType/, e.message
+    truthy 'should throw TypeError', e instanceof TypeError
+
+  it 'phrases error messages correctly', ->
+    e = throws -> notHasType Object, {}
+    match /not to be of type Object/, e.message
+
+  it 'recognizes non-Strings', ->
+    notHasType String, 42
+    throws -> notHasType String, '42'
+
+  it 'recognizes non-Numbers', ->
+    notHasType Number, '42'
+    throws -> notHasType Number, 42
+
+  it 'recognizes non-NaNs', ->
+    notHasType NaN, 6502
+    throws 'NaN tested as not being NaN', -> notHasType NaN, NaN
+
+  it 'recognizes non-RegExps', ->
+    notHasType RegExp, '/howdy/'
+    throws -> notHasType RegExp, /howdy/
+
+  it 'recognizes non-Arrays', ->
+    notHasType Array, '[1, 2, 3]'
+    throws -> notHasType Array, [1, 2, 3]
+
+  it 'recognizes non-Functions', ->
+    notHasType Function, 'function () { }'
+    throws -> notHasType Function, ->
+
+  it 'recognizes non-Objects', ->
+    notHasType Object, [1, 2, 3]
+    throws -> notHasType Object, foo: 42
+
+  it 'recognizes non-Dates', ->
+    notHasType Date, getTime: -> 0
+    notHasType Date, new Date 'Invalid Date'
+    throws 'Date tested as not being a Date', -> notHasType Date, new Date
+
+  it 'recognizes not-null', ->
+    notHasType null, undefined
+    throws 'null tested as not being null', -> notHasType null, null
+
+  it 'recognizes not-undefined', ->
+    notHasType undefined, null
+    throws -> notHasType undefined, undefined
+
+describe 'rejects', ->
+  it 'errors synchronously on non-promise', ->
+    match /^rejects expects/, throws(-> rejects 42).message
+
+  it 'resolves for a rejected promise', ->
+    equal 'kittens', rejects Promise.reject 'kittens'
+
+  it 'rejects a resolved promise', ->
+    equal "Promise wasn't rejected as expected to",
+      rejects(rejects Promise.resolve 42).get('message')
+
+describe 'resolves', ->
+  it 'errors synchronously on non-promise', ->
+    match /^resolves expects/, throws(-> resolves {}).message
+
+  it 'rejects for a rejected promise', ->
+    include 'Promise was rejected despite resolves assertion:\n42',
+      rejects(resolves Promise.reject new Error 42).get('message')
+
+  it 'resolves for a resolved promise', ->
+    equal 'kittens', resolves Promise.resolve 'kittens'
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 0000000..69b4f0c
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,4 @@
+--check-leaks
+--colors
+--compilers test.coffee:coffee-script/register
+--recursive
diff --git a/test/promisified.test.coffee b/test/promisified.test.coffee
new file mode 100644
index 0000000..b81ce80
--- /dev/null
+++ b/test/promisified.test.coffee
@@ -0,0 +1,96 @@
+# test the auto-promise-awarified versions of common tests
+
+Promise = require 'bluebird'
+assert  = require '../lib/assertive'
+
+syncFuncs =
+  truthy:
+    pass:
+      args: [true], descr: 'a truthy promise resolution'
+    fail:
+      args: [false]
+      descr: 'a falsey promise resolution'
+      explain: 'resolves to true'
+
+  equal:
+    pass: args: [5, 5], descr: 'an equal promise resolution'
+    fail:
+      args: [5, 6]
+      descr: 'an inequal promise resolution'
+      explain: '5 is 5'
+
+  deepEqual:
+    pass:
+      args: [['a', 'b'], ['a', 'b']]
+      descr: 'a deep equal promise resolution'
+    fail:
+      args: [['a', 'b'], 'x']
+      descr: 'a deep inequal promise resolution'
+      explain: 'a,b is a,b'
+
+  include:
+    pass:
+      args: ['x', 'fox']
+      descr: 'needle inclusion in haystack promise resolution'
+    fail:
+      args: ['x', 'dog']
+      descr: 'needle exclusion from haystack promise resolution'
+      explain: 'x in word'
+
+  match:
+    pass: args: [/x/, 'fox'], descr: 'match /x/'
+    fail:
+      args: [/x/, 'dog']
+      descr: 'needle exclusion from haystack promise resolution'
+      explain: '/x/ matches word'
+
+  throws:
+    pass:
+      args: [-> throw 'foo']
+      descr: 'a promise for an excepting function'
+    fail:
+      args: [ -> 'foo' ]
+      descr: 'a promise for a non-excepting function'
+      explain: 'function throws an exception'
+
+  notThrows:
+    pass:
+      args: [-> 42]
+      descr: 'a non-excepting function'
+    fail:
+      args: [ -> throw 'foo' ]
+      descr: 'a promise for an excepting function'
+      explain: 'function does not throw an exception'
+
+  hasType:
+    pass:
+      args: [Boolean, true]
+      descr: 'matched type on promise resolution'
+    fail:
+      args: [Boolean, 'true']
+      descr: 'mismatched type on promise resolution'
+      explain: 'result is a boolean'
+
+describe 'promise-aware functionality', ->
+  for name, bits of syncFuncs
+    do (name, bits) ->
+      {pass, fail} = bits
+      for pf, {args} of bits
+        # replace last argument with promised resolution of same
+        bits[pf].pargs = args[0...args.length-1].concat(
+          [Promise.resolve(args[args.length-1])])
+
+      describe name, ->
+        it 'returns a promise when passed a promise', ->
+          assert.expect assert[name](pass.pargs...) instanceof Promise
+
+        it 'does not return a promise when not passed one', ->
+          assert.expect not (assert[name](pass.args...) instanceof Promise)
+
+        it "resolves for #{pass.descr}", ->
+          assert.resolves "#{name} should succeed", assert[name] pass.pargs...
+
+        it "rejects for #{fail.descr}", ->
+          assert.rejects "#{name} should throw",
+            assert[name] fail.explain, fail.pargs...
+          .then (err) -> assert.include fail.explain, err.message

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-assertive.git



More information about the Pkg-javascript-commits mailing list