Skip to content

Commit

Permalink
Merge pull request #93 from oss-specs/feature/goingBeta
Browse files Browse the repository at this point in the history
Going beta
  • Loading branch information
sponte committed Sep 1, 2015
2 parents 96b05d1 + 6e68378 commit 2f058ee
Show file tree
Hide file tree
Showing 18 changed files with 311 additions and 83 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.idea
public/feature-files/*

# Repo meta data
project-data

# Logs
logs
*.log
Expand Down
9 changes: 0 additions & 9 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,6 @@ app.use(function(req, res, next) {
next(err);
});

// Development error handler.
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.send(err);
});
}

// Production error handler (won't hit this if the dev one is set).
app.use(function(err, req, res, next) {
var status = err.status || 500;
var errorMessage = err.message || err;
Expand Down
13 changes: 8 additions & 5 deletions features-support/step_definitions/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ module.exports = function () {
this.Given(/^a set of specifications containing at least one feature file\.?$/, function (callback) {
var world = this;

// Only generate the static test data once.
// Only generate the static test data once for the feature
// as opposed to the @cleanSlate tag which removes it for
// each scenario.
if (staticTestDataExists) {
callback();
return;
}

// Make sure the test data is clean.
// Make sure the test data is removed.
world.deleteTestSpecs()
.then(function() {
return world.createSpecsForTesting();
Expand Down Expand Up @@ -71,7 +73,7 @@ module.exports = function () {
});

this.Then(/^the list of features will be visible\.?$/, function (callback) {
should.equal(this.statusCode, 200, "Bad HTTP status code.");
should.equal(this.statusCode, 200, "Bad HTTP status code: " + this.statusCode + "\nBody:\n" + this.body);
should.equal(
/\.feature/i.test(this.body) && /\.md/i.test(this.body),
true,
Expand All @@ -86,14 +88,15 @@ module.exports = function () {
callback(error);
return;
}
world.firstFeatureLink = (/class="speclink" href="([\w\/.-]+)\.feature"/.exec(body))[1];
world.firstFeatureLink = (/class="spec-link" href="([\w\/.-]+)\.feature"/.exec(body))[1];
callback();
});
});

this.When(/^an interested party wants to view the scenarios within that feature file\.?$/, function (callback) {
var world = this; // the World variable is passed around the step defs as `this`.
var featurePath = 'http://localhost:' + world.appPort + '/' + world.firstFeatureLink;

request
.get(featurePath, function(error, response, body) {
if (error) {
Expand All @@ -107,7 +110,7 @@ module.exports = function () {
});

this.Then(/^the scenarios will be visible\.?$/, function (callback) {
should.equal(this.statusCode, 200, "Bad HTTP status code.");
should.equal(this.statusCode, 200, "Bad HTTP status code: " + this.statusCode + "\nBody:\n" + this.body);
should.equal(/feature:/i.test(this.body),
true,
"The returned document body does not contain the word 'feature'");
Expand Down
30 changes: 28 additions & 2 deletions features-support/world.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"use strict";

var fs = require("q-io/fs"); // https://github.com/kriskowal/q-io
var path = require('path');

// Get the project metadata module so we can inject test data.
var projectMetaData = require('../lib/specifications/projectMetaData');
var featureFileRoot = path.join(__dirname, '..', 'public', 'feature-files');

module.exports = function() {
this.World = function World(callback) {
this.appPort = process.env.PORT || 3000;
this.paths = {
features: 'features',
public: 'public/feature-files/'
features: path.join('features'),
public: path.join('public', 'feature-files'),
data: path.join('project-data')
};

/**
Expand All @@ -21,15 +27,35 @@ module.exports = function() {
return fs.makeTree(world.paths.public)
.then(function() {
return fs.copyTree(world.paths.features, world.paths.public);
})
.then(function() {

// Configure the metadata module with the feature file storage path.
var configuredDeriveAndStore = projectMetaData.deriveAndStore(featureFileRoot);

// Pass an object of made up repo data to be decorated with
// feature file paths and return a promise for completion
// of storage of that data.
return configuredDeriveAndStore({
repoName: 'made up',
repoUrl: 'http//example.com',
head: 'testing!',
localName: 'not a real repo'
});
});
};

/**
* Remove any specs and data already in place.
*
* @return promise for operation completion.
*/
this.deleteTestSpecs = function() {
var world = this;
return fs.removeTree(world.paths.public)
.then(function() {
return fs.removeTree(world.paths.data);
})
.catch(function(err) {
// Ignore failure to unlink missing directory.
if (err.code !== 'ENOENT') {
Expand Down
2 changes: 1 addition & 1 deletion features/presenting/specifications-are-visible.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Feature: Specifications are visible to users
As an interested party
I want specifications to be visible in a nice web UI.

The general idea is that anyone should be able to see the specifications stored in a project without having to understand how version control works. The 'server' tag means that test requires the server to be running.
The general idea is that anyone should be able to see the specifications stored in a project without having to understand how version control works. The 'server' tag means that test requires the server to be running. These tests copy features from this repo so don't need internet access.

Background:
Given a set of specifications containing at least one feature file
Expand Down
21 changes: 17 additions & 4 deletions lib/specifications/getProject.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
"use strict";

var path = require('path');

var getProjectUsingGit = require('./getProjectUsingGit');
var projectMetaData = require('./projectMetaData');

// TODO: Remove magic knowledge of where feature files are, injection?
var featureFileRoot = path.join(__dirname, '..', '..', 'public', 'feature-files');

/**
* Get a copy of the project.
* Get a copy of the project, derive metadata, store metadata.
*
* @return a promise for the completion of the operation.
* @return a promise for the completion of repo metadata storage.
*/
module.exports = function getProject(repoUrl) {
var repoName = /\/([^\/]+?)(?:\.git)?\/?$/.exec(repoUrl);
repoName = (repoName && repoName.length ? repoName[1] : false);
if (!repoName) throw new TypeError("Could not determine repository name.");
if (!repoName) {
throw new TypeError("Could not determine repository name.");
}

// Path for local cloning.
var localName = path.join(featureFileRoot, repoName);

return getProjectUsingGit(repoUrl, repoName);
// Clone or update the repo then derive and store the project metadata.
return getProjectUsingGit(repoUrl, repoName, localName)
.then(projectMetaData.deriveAndStore(featureFileRoot));
};
32 changes: 23 additions & 9 deletions lib/specifications/getProjectUsingGit.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ var fs = require("q-io/fs"); // https://github.com/kriskowal/q-io
*
* @return a PROMISE for the HEAD commit hash of the repo.
*/
module.exports = function getProject(repoUrl, repoName) {

// TODO get rid of magic knowledge of 'public/feature-files'.
var localName = path.join(__dirname, "../../public/feature-files", repoName);
module.exports = function getProject(repoUrl, repoName, localName) {
var repository;
var branchName;

function generateRepoMetaData(sha) {
return {
repoName: repoName,
repoUrl: repoUrl,
localName: localName,
commit: sha,
shortCommit: sha.substring(0, 7)
};
}

return fs.exists(localName)
.then(function(pathExists) {

Expand All @@ -24,16 +31,19 @@ module.exports = function getProject(repoUrl, repoName) {
certificateCheck: function() { return 1; }
};

// If there is no matching directory then CLONE the repo.
// If there is no matching directory then **CLONE** the repo.
if (!pathExists) {
var cloneOptions = {};
cloneOptions.remoteCallbacks = copingWithMacCertBug;
return git.Clone(repoUrl, localName, cloneOptions)
.then(function(repository) {
return repository.getHeadCommit();
.then(function(repo) {
return repo.getHeadCommit();
})
.then(function(commit) {
return generateRepoMetaData(commit.sha());
});

// Else the repo probably exists, perform a PULL on it.
// Else the repo probably exists, perform a **PULL** on it.
// https://github.com/nodegit/nodegit/issues/341
// https://github.com/nodegit/nodegit/commit/dc814a45268305e56c99db64efd5d0fe8bbbb8c2
} else {
Expand All @@ -49,7 +59,11 @@ module.exports = function getProject(repoUrl, repoName) {
.then(function() {
// Should be a fast-forward merge.
// Will return the commit hash of merge, hopefully matching remote HEAD.
return repository.mergeBranches(branchName, "origin/" + branchName);
return repository
.mergeBranches(branchName, "origin/" + branchName)
.then(function(commitHash) {
return generateRepoMetaData(commitHash);
});
});
}
});
Expand Down
38 changes: 38 additions & 0 deletions lib/specifications/projectDataStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

var fs = require('fs');
var qfs = require('q-io/fs'); // https://github.com/kriskowal/q-io
var path = require('path');

// TODO: don't like hardcoding this.
var appRoot = path.join(__dirname, '..', '..');

// Stick the data in a project-data subdirectory.
var projectDataDirectory = path.join(appRoot, 'project-data');

module.exports = {

// Return promise for write completion.
persist: function(projectData) {

// Ensure project data directory exists and write the file.
fs.existsSync(projectDataDirectory) || fs.mkdirSync(projectDataDirectory);
return qfs.write(path.join(projectDataDirectory, projectData.name + '.data'), JSON.stringify(projectData));
},

// Return promise for an array of paths in data directory
// with the .data file extension.
getNames: function() {
return qfs.listTree(projectDataDirectory, function guard(path) {
return /\.(data)$/.test(path);
});
},

// Return promise for data.
get: function(dataFilePath) {
return qfs.read(dataFilePath)
.then(function(metaDataJsonString) {
return JSON.parse(metaDataJsonString);
});
}
};
70 changes: 70 additions & 0 deletions lib/specifications/projectMetaData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use strict";

var path = require('path');

var getFeatureFilePaths = require('./getFeatureFilePaths');
var projectDataStorage = require('./projectDataStorage');

// Get a function for use with Array.prototype.map to
// convert file paths for feature files to express routes.
function pathsToRoutes(featureFileRoot) {
return function(featurePath) {

// Remove the filesystem root.
featurePath = featurePath.replace(featureFileRoot, '');

// Prefix with the 'features' route, always use backslashes.
featurePath = path.posix.join('features', featurePath);

return {
featureRoute: featurePath,
featureName: featurePath.replace('.feature', '').replace('features/', '')
};
}
}


/**
* Takes an object of repo data and returns a promise for that
* data decorated with feature file routes.
*
* Feature file routes are taken from disk via `getFeatureFilePaths`.
*
* @return A promise for the decorated data object.
*/
function deriveProjectData(featureFileRoot, repoData) {
var projectData = {
name: repoData.repoName,
url: repoData.repoUrl,
commit: repoData.commit,
shortCommit: repoData.shortCommit,
localName: repoData.localName,
featureFilePaths: []
}

// Get the paths to the feature files.
return getFeatureFilePaths(featureFileRoot)
.then(function(featureFilePaths) {

// Map from the storage directory to the Express route for creating links.
projectData.featureFilePaths = featureFilePaths.map(pathsToRoutes(featureFileRoot));
return projectData;
});
}


/**
* Return a function which takes the repo data, derives the project data and stores it.
*
* @return A promise for completion of the storage of the project data.
*/
function deriveAndStore(featureFileRoot) {
return function(repoData) {
return deriveProjectData(featureFileRoot, repoData)
.then(projectDataStorage.persist);
}
}

module.exports = {
deriveAndStore: deriveAndStore
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "specs",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"bin": {
"module-name": "./bin/www"
Expand Down Expand Up @@ -55,6 +55,7 @@
"morgan": "~1.6.0",
"newrelic": "^1.20.2",
"nodegit": "^0.4.1",
"q": "^1.4.1",
"q-io": "^1.13.1",
"serve-favicon": "~2.3.0"
},
Expand Down
Loading

0 comments on commit 2f058ee

Please sign in to comment.