From 9ae7cd10c5423cbad9d38fc6c99960c62ac2f429 Mon Sep 17 00:00:00 2001 From: darsain Date: Wed, 7 Sep 2016 01:14:37 +0200 Subject: [PATCH] Breaking: Normalize paths (closes #80) (#101) --- README.md | 41 +++- index.js | 69 +++++-- lib/normalize.js | 5 + lib/stripTrailingSep.js | 11 + test/File.js | 422 ++++++++++++++++++++++++++++++++------- test/normalize.js | 15 ++ test/stripTrailingSep.js | 36 ++++ 7 files changed, 507 insertions(+), 92 deletions(-) create mode 100644 lib/normalize.js create mode 100644 lib/stripTrailingSep.js create mode 100644 test/normalize.js create mode 100644 test/stripTrailingSep.js diff --git a/README.md b/README.md index d4d2c61..c80cf91 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ File.isCustomProp('path'); // false -> internal getter/setter Read more in [Extending Vinyl](#extending-vinyl). ### constructor(options) +All internally managed paths (`cwd`, `base`, `path`, `[history]`) are normalized and remove a trailing separator. + #### options.cwd Type: `String`

Default: `process.cwd()` @@ -64,7 +66,7 @@ Path history. Has no effect if `options.path` is passed. Type: `Array`

Default: `options.path ? [options.path] : []` #### options.stat -The result of an fs.stat call. See [fs.Stats](http://nodejs.org/api/fs.html#fs_class_fs_stats) for more information. +The result of an fs.stat call. This is how you mark the file as a directory. See [isDirectory()](#isDirectory) and [fs.Stats](http://nodejs.org/api/fs.html#fs_class_fs_stats) for more information. Type: `fs.Stats`

Default: `null` @@ -92,6 +94,15 @@ Returns true if file.contents is a Stream. ### isNull() Returns true if file.contents is null. +### isDirectory() +Returns true if file is a directory. File is considered a directory when: + +- `file.isNull()` is `true` +- `file.stat` is an object +- `file.stat.isDirectory()` returns `true` + +When constructing a Vinyl object, pass in a valid `fs.Stats` object via `options.stat`. Some operations in Vinyl might need to know the file is a directory from the get go. If you are mocking the `fs.Stats` object, ensure it has the `isDirectory()` method. + ### clone([opt]) Returns a new File object with all attributes cloned.
By default custom attributes are deep-cloned. @@ -124,8 +135,14 @@ if (file.isBuffer()) { } ``` +### cwd +Gets and sets current working directory. Defaults to `process.cwd`. Will always be normalized and remove a trailing separator. + +### base +Gets and sets base directory. Used for relative pathing (typically where a glob starts). When `null` or `undefined`, it simply proxies the `file.cwd` property. Will always be normalized and remove a trailing separator. + ### path -Absolute pathname string or `undefined`. Setting to a different value pushes the old value to `history`. +Absolute pathname string or `undefined`. Setting to a different value pushes the old value to `history`. All new values are normalized and remove a trailing separator. ### history Array of `path` values the file object has had, from `history[0]` (original) through `history[history.length - 1]` (current). `history` and its elements should normally be treated as read-only and only altered indirectly by setting `path`. @@ -146,7 +163,7 @@ console.log(file.relative); // file.coffee ``` ### dirname -Gets and sets path.dirname for the file path. +Gets and sets path.dirname for the file path. Will always be normalized and remove a trailing separator. Example: @@ -225,6 +242,24 @@ console.log(file.extname); // .js console.log(file.path); // /test/file.js ``` +### symlink +Path where the file points to in case it's a symbolic link. Will always be normalized and remove a trailing separator. + +## Normalization and concatenation +Since all properties are normalized in their setters, you can just concatenate with `/`, and normalization takes care of it properly on all platforms. + +Example: + +```javascript +var file = new File(); +file.path = '/' + 'test' + '/' + 'foo.bar'; +console.log(file.path); +// posix => /test/foo.bar +// win32 => \\test\\foo.bar +``` + +But never concatenate with `\`, since that is a valid filename character on posix system. + ## Extending Vinyl When extending Vinyl into your own class with extra features, you need to think about a few things. diff --git a/index.js b/index.js index 0cdbdb3..975f36c 100644 --- a/index.js +++ b/index.js @@ -2,15 +2,18 @@ var path = require('path'); var clone = require('clone'); var cloneStats = require('clone-stats'); var cloneBuffer = require('./lib/cloneBuffer'); +var stripTrailingSep = require('./lib/stripTrailingSep'); var isBuffer = require('./lib/isBuffer'); var isStream = require('./lib/isStream'); var isNull = require('./lib/isNull'); var inspectStream = require('./lib/inspectStream'); +var normalize = require('./lib/normalize'); var Stream = require('stream'); var replaceExt = require('replace-ext'); var builtInFields = [ - '_contents', '_symlink', 'contents', 'stat', 'history', 'path', 'base', 'cwd', + '_contents', '_symlink', 'contents', 'stat', 'history', 'path', + '_base', 'base', '_cwd', 'cwd', ]; function File(file) { @@ -20,19 +23,22 @@ function File(file) { file = {}; } - // Record path change - var history = file.path ? [file.path] : file.history; - this.history = history || []; - - this.cwd = file.cwd || process.cwd(); - this.base = file.base || this.cwd; - // Stat = files stats object this.stat = file.stat || null; // Contents = stream, buffer, or null if not read this.contents = file.contents || null; + // Replay path history to ensure proper normalization and trailing sep + var history = file.path ? [file.path] : file.history || []; + this.history = []; + history.forEach(function(path) { + self.path = path; + }); + + this.cwd = file.cwd || process.cwd(); + this.base = file.base; + this._isVinyl = true; this._symlink = null; @@ -156,7 +162,7 @@ File.prototype.inspect = function() { var inspect = []; // Use relative path if possible - var filePath = (this.base && this.path) ? this.relative : this.path; + var filePath = this.path ? this.relative : null; if (filePath) { inspect.push('"' + filePath + '"'); @@ -195,12 +201,40 @@ Object.defineProperty(File.prototype, 'contents', { }, }); +Object.defineProperty(File.prototype, 'cwd', { + get: function() { + return this._cwd; + }, + set: function(cwd) { + if (!cwd || typeof cwd !== 'string') { + throw new Error('cwd must be a non-empty string.'); + } + this._cwd = stripTrailingSep(normalize(cwd)); + }, +}); + +Object.defineProperty(File.prototype, 'base', { + get: function() { + return this._base || this._cwd; + }, + set: function(base) { + if (base == null) { + delete this._base; + return; + } + if (typeof base !== 'string' || !base) { + throw new Error('base must be a non-empty string, or null/undefined.'); + } + base = stripTrailingSep(normalize(base)); + if (base !== this._cwd) { + this._base = base; + } + }, +}); + // TODO: Should this be moved to vinyl-fs? Object.defineProperty(File.prototype, 'relative', { get: function() { - if (!this.base) { - throw new Error('No base specified! Can not get relative.'); - } if (!this.path) { throw new Error('No path specified! Can not get relative.'); } @@ -222,7 +256,7 @@ Object.defineProperty(File.prototype, 'dirname', { if (!this.path) { throw new Error('No path specified! Can not set dirname.'); } - this.path = path.join(dirname, path.basename(this.path)); + this.path = path.join(dirname, this.basename); }, }); @@ -237,7 +271,7 @@ Object.defineProperty(File.prototype, 'basename', { if (!this.path) { throw new Error('No path specified! Can not set basename.'); } - this.path = path.join(path.dirname(this.path), basename); + this.path = path.join(this.dirname, basename); }, }); @@ -253,7 +287,7 @@ Object.defineProperty(File.prototype, 'stem', { if (!this.path) { throw new Error('No path specified! Can not set stem.'); } - this.path = path.join(path.dirname(this.path), stem + this.extname); + this.path = path.join(this.dirname, stem + this.extname); }, }); @@ -278,8 +312,9 @@ Object.defineProperty(File.prototype, 'path', { }, set: function(path) { if (typeof path !== 'string') { - throw new Error('path should be string'); + throw new Error('path should be a string.'); } + path = stripTrailingSep(normalize(path)); // Record history only when path changed if (path && path !== this.path) { @@ -298,7 +333,7 @@ Object.defineProperty(File.prototype, 'symlink', { throw new Error('symlink should be a string'); } - this._symlink = symlink; + this._symlink = stripTrailingSep(normalize(symlink)); }, }); diff --git a/lib/normalize.js b/lib/normalize.js new file mode 100644 index 0000000..5bb5537 --- /dev/null +++ b/lib/normalize.js @@ -0,0 +1,5 @@ +var normalize = require('path').normalize; + +module.exports = function(str) { + return str === '' ? str : normalize(str); +}; diff --git a/lib/stripTrailingSep.js b/lib/stripTrailingSep.js new file mode 100644 index 0000000..ecc3765 --- /dev/null +++ b/lib/stripTrailingSep.js @@ -0,0 +1,11 @@ +module.exports = function(str) { + while (endsInSeparator(str)) { + str = str.slice(0, -1); + } + return str; +}; + +function endsInSeparator(str) { + var last = str[str.length - 1]; + return str.length > 1 && (last === '/' || last === '\\'); +} diff --git a/test/File.js b/test/File.js index 1aaebc7..351f147 100644 --- a/test/File.js +++ b/test/File.js @@ -31,7 +31,7 @@ describe('File', function() { }); it('should default base to cwd', function(done) { - var cwd = '/'; + var cwd = path.normalize('/'); var file = new File({ cwd: cwd }); file.base.should.equal(cwd); done(); @@ -68,21 +68,21 @@ describe('File', function() { }); it('should set base to given value', function(done) { - var val = '/'; + var val = path.normalize('/'); var file = new File({ base: val }); file.base.should.equal(val); done(); }); it('should set cwd to given value', function(done) { - var val = '/'; + var val = path.normalize('/'); var file = new File({ cwd: val }); file.cwd.should.equal(val); done(); }); it('should set path to given value', function(done) { - var val = '/test.coffee'; + var val = path.normalize('/test.coffee'); var file = new File({ path: val }); file.path.should.equal(val); file.history.should.eql([val]); @@ -90,7 +90,7 @@ describe('File', function() { }); it('should set history to given value', function(done) { - var val = '/test.coffee'; + var val = path.normalize('/test.coffee'); var file = new File({ history: [val] }); file.path.should.equal(val); file.history.should.eql([val]); @@ -117,6 +117,71 @@ describe('File', function() { file.sourceMap.should.equal(sourceMap); done(); }); + + it('should normalize path', function() { + var file = new File({ path: '/test/foo/../test.coffee' }); + + if (process.platform === 'win32') { + file.path.should.equal('\\test\\test.coffee'); + file.history.should.eql(['\\test\\test.coffee']); + } else { + file.path.should.equal('/test/test.coffee'); + file.history.should.eql(['/test/test.coffee']); + } + }); + + it('should correctly normalize and strip trailing sep from path', function() { + var file = new File({ path: '/test/foo/../foo/' }); + + if (process.platform === 'win32') { + file.path.should.equal('\\test\\foo'); + } else { + file.path.should.equal('/test/foo'); + } + }); + + it('should correctly normalize and strip trailing sep from history', function() { + var file = new File({ + history: [ + '/test/foo/../foo/', + '/test/bar/../bar/', + ], + }); + + if (process.platform === 'win32') { + file.history.should.eql([ + '\\test\\foo', + '\\test\\bar', + ]); + } else { + file.history.should.eql([ + '/test/foo', + '/test/bar', + ]); + } + }); + + it('should normalize history', function() { + var history = [ + '/test/bar/../bar/test.coffee', + '/test/foo/../test.coffee', + ]; + var file = new File({ history: history }); + + if (process.platform === 'win32') { + file.path.should.equal('\\test\\test.coffee'); + file.history.should.eql([ + '\\test\\bar\\test.coffee', + '\\test\\test.coffee', + ]); + } else { + file.path.should.equal('/test/test.coffee'); + file.history.should.eql([ + '/test/bar/test.coffee', + '/test/test.coffee', + ]); + } + }); }); describe('isBuffer()', function() { @@ -367,9 +432,9 @@ describe('File', function() { it('should properly clone the `history` property', function(done) { var options = { - cwd: '/', - base: '/test/', - path: '/test/test.js', + cwd: path.normalize('/'), + base: path.normalize('/test/'), + path: path.normalize('/test/test.js'), contents: new Buffer('test'), stat: fs.statSync(__filename), }; @@ -413,23 +478,20 @@ describe('File', function() { path: '/test/test.coffee', contents: null, }; + var history = [ + path.normalize('/test/test.coffee'), + path.normalize('/test/test.js'), + path.normalize('/test/test-938di2s.js'), + ]; var file = new File(options); - file.path = '/test/test.js'; - file.path = '/test/test-938di2s.js'; + file.path = history[1]; + file.path = history[2]; var file2 = file.clone(); - file2.history.should.eql([ - '/test/test.coffee', - '/test/test.js', - '/test/test-938di2s.js', - ]); - file2.history.should.not.equal([ - '/test/test.coffee', - '/test/test.js', - '/test/test-938di2s.js', - ]); - file2.path.should.eql('/test/test-938di2s.js'); + file2.history.should.eql(history); + file2.history.should.not.equal(history); + file2.path.should.eql(history[2]); done(); }); @@ -652,18 +714,6 @@ describe('File', function() { done(); }); - it('should return correct format when Buffer and only path and no base', function(done) { - var val = new Buffer('test'); - var file = new File({ - cwd: '/', - path: '/test/test.coffee', - contents: val, - }); - delete file.base; - file.inspect().should.equal('>'); - done(); - }); - it('should return correct format when Stream and relative path', function(done) { var file = new File({ cwd: '/', @@ -724,23 +774,136 @@ describe('File', function() { }); }); - describe('relative get/set', function() { - it('should error on set', function(done) { + describe('cwd get/set', function() { + it('should return _cwd', function() { var file = new File(); - try { - file.relative = 'test'; - } catch (err) { - should.exist(err); - done(); + file.cwd = '/test'; + file.cwd.should.equal(file._cwd); + }); + + it('should set cwd', function() { + var file = new File(); + file.cwd = '/test'; + file._cwd.should.equal(path.normalize('/test')); + }); + + it('should normalize and strip trailing sep on set', function() { + var file = new File(); + + file.cwd = '/test/foo/../foo/'; + + if (process.platform === 'win32') { + file.cwd.should.equal('\\test\\foo'); + } else { + file.cwd.should.equal('/test/foo'); + } + + file.cwd = '\\test\\foo\\..\\foo\\'; + + if (process.platform === 'win32') { + file.cwd.should.equal('\\test\\foo'); + } else { + file.cwd.should.equal('\\test\\foo\\..\\foo'); } }); - it('should error on get when no base', function(done) { - var a; + it('should throw on set when value is empty or not a string', function() { + var notAllowed = [ + '', null, undefined, true, false, 0, Infinity, NaN, {}, [], + ]; + notAllowed.forEach(function(val) { + (function() { + new File().cwd = val; + }).should.throw('cwd must be a non-empty string.'); + }); + }); + }); + + describe('base get/set', function() { + it('should proxy to cwd when omitted', function() { + var file = new File({ + cwd: '/test', + }); + file.base.should.equal(path.normalize('/test')); + }); + + it('should proxy to cwd when same', function() { + var file = new File({ + cwd: '/test', + base: '/test', + }); + file.cwd = '/foo/'; + file.base.should.equal(path.normalize('/foo')); + + var file2 = new File({ + cwd: '/test', + }); + file2.base = '/test/'; + file2.cwd = '/foo/'; + file2.base.should.equal(path.normalize('/foo')); + }); + + it('should proxy to cwd when null or undefined', function() { + var file = new File({ + cwd: '/foo', + base: '/bar', + }); + file.base.should.equal(path.normalize('/bar')); + file.base = null; + file.base.should.equal(path.normalize('/foo')); + file.base = '/bar/'; + file.base.should.equal(path.normalize('/bar')); + file.base = undefined; + file.base.should.equal(path.normalize('/foo')); + }); + + it('should return _base', function() { + var file = new File(); + file._base = '/test/'; + file.base.should.equal('/test/'); + }); + + it('should set base', function() { + var file = new File(); + file.base = '/test/foo'; + file.base.should.equal(path.normalize('/test/foo')); + }); + + it('should normalize and strip trailing sep on set', function() { + var file = new File(); + + file.base = '/test/foo/../foo/'; + + if (process.platform === 'win32') { + file.base.should.equal('\\test\\foo'); + } else { + file.base.should.equal('/test/foo'); + } + + file.base = '\\test\\foo\\..\\foo\\'; + + if (process.platform === 'win32') { + file.base.should.equal('\\test\\foo'); + } else { + file.base.should.equal('\\test\\foo\\..\\foo'); + } + }); + + it('should throw on set when not null/undefined or a non-empty string', function() { + var notStrings = [true, false, 1, 0, Infinity, NaN, '', {}, []]; + notStrings.forEach(function(val) { + (function() { + new File().base = val; + }).should.throw('base must be a non-empty string, or null/undefined.'); + }); + }); + }); + + describe('relative get/set', function() { + it('should error on set', function(done) { var file = new File(); - delete file.base; try { - a = file.relative; + file.relative = 'test'; } catch (err) { should.exist(err); done(); @@ -776,6 +939,35 @@ describe('File', function() { file.relative.should.equal(path.join('test','test.coffee')); done(); }); + + it('should not append sep when directory', function() { + var file = new File({ + base: '/test', + path: '/test/foo/bar', + stat: { + isDirectory: function() { + return true; + }, + }, + }); + file.relative.should.equal(path.normalize('foo/bar')); + }); + + it('should not append sep when directory & simlink', function() { + var file = new File({ + base: '/test', + path: '/test/foo/bar', + stat: { + isDirectory: function() { + return true; + }, + isSymbolicLink: function() { + return true; + }, + }, + }); + file.relative.should.equal(path.normalize('foo/bar')); + }); }); describe('dirname get/set', function() { @@ -790,13 +982,13 @@ describe('File', function() { } }); - it('should return the dirname of the path', function(done) { + it('should return the path without trailing sep', function(done) { var file = new File({ cwd: '/', - base: '/test/', + base: '/test', path: '/test/test.coffee', }); - file.dirname.should.equal('/test'); + file.dirname.should.equal(path.normalize('/test')); done(); }); @@ -817,7 +1009,7 @@ describe('File', function() { path: '/test/test.coffee', }); file.dirname = '/test/foo'; - file.path.should.equal('/test/foo/test.coffee'); + file.path.should.equal(path.normalize('/test/foo/test.coffee')); done(); }); }); @@ -844,6 +1036,41 @@ describe('File', function() { done(); }); + it('should not append trailing sep', function() { + var file = new File({ + path: '/test/foo', + stat: { + isDirectory: function() { + return true; + }, + }, + }); + file.basename.should.equal('foo'); + + var file2 = new File({ + path: '/test/foo', + stat: { + isSymbolicLink: function() { + return true; + }, + }, + }); + file2.basename.should.equal('foo'); + + var file3 = new File({ + path: '/test/foo', + stat: { + isDirectory: function() { + return true; + }, + isSymbolicLink: function() { + return true; + }, + }, + }); + file3.basename.should.equal('foo'); + }); + it('should error on set when no path', function(done) { var file = new File(); try { @@ -861,7 +1088,7 @@ describe('File', function() { path: '/test/test.coffee', }); file.basename = 'foo.png'; - file.path.should.equal('/test/foo.png'); + file.path.should.equal(path.normalize('/test/foo.png')); done(); }); }); @@ -905,7 +1132,7 @@ describe('File', function() { path: '/test/test.coffee', }); file.stem = 'foo'; - file.path.should.equal('/test/foo.coffee'); + file.path.should.equal(path.normalize('/test/foo.coffee')); done(); }); }); @@ -949,21 +1176,21 @@ describe('File', function() { path: '/test/test.coffee', }); file.extname = '.png'; - file.path.should.equal('/test/test.png'); + file.path.should.equal(path.normalize('/test/test.png')); done(); }); }); describe('path get/set', function() { - it('should record history when instantiation', function() { var file = new File({ cwd: '/', path: '/test/test.coffee', }); + var history = [path.normalize('/test/test.coffee')]; - file.path.should.eql('/test/test.coffee'); - file.history.should.eql(['/test/test.coffee']); + file.path.should.eql(history[0]); + file.history.should.eql(history); }); it('should record history when path change', function() { @@ -971,31 +1198,38 @@ describe('File', function() { cwd: '/', path: '/test/test.coffee', }); + var history = [ + path.normalize('/test/test.coffee'), + path.normalize('/test/test.js'), + ]; + + file.path = history[history.length - 1]; + file.path.should.eql(history[history.length - 1]); + file.history.should.eql(history); - file.path = '/test/test.js'; - file.path.should.eql('/test/test.js'); - file.history.should.eql(['/test/test.coffee', '/test/test.js']); + history.push(path.normalize('/test/test.es6')); - file.path = '/test/test.coffee'; - file.path.should.eql('/test/test.coffee'); - file.history.should.eql(['/test/test.coffee', '/test/test.js', '/test/test.coffee']); + file.path = history[history.length - 1]; + file.path.should.eql(history[history.length - 1]); + file.history.should.eql(history); }); it('should not record history when set the same path', function() { + var val = path.normalize('/test/test.coffee'); var file = new File({ cwd: '/', - path: '/test/test.coffee', + path: val, }); - file.path = '/test/test.coffee'; - file.path = '/test/test.coffee'; - file.path.should.eql('/test/test.coffee'); - file.history.should.eql(['/test/test.coffee']); + file.path = val; + file.path = val; + file.path.should.eql(val); + file.history.should.eql([val]); // Ignore when set empty string file.path = ''; - file.path.should.eql('/test/test.coffee'); - file.history.should.eql(['/test/test.coffee']); + file.path.should.eql(val); + file.history.should.eql([val]); }); it('should throw when set path null', function() { @@ -1009,7 +1243,39 @@ describe('File', function() { (function() { file.path = null; - }).should.throw('path should be string'); + }).should.throw('path should be a string.'); + }); + + it('should normalize the path on set', function() { + var file = new File(); + + file.path = '/test/foo/../test.coffee'; + + if (process.platform === 'win32') { + file.path.should.equal('\\test\\test.coffee'); + file.history.should.eql(['\\test\\test.coffee']); + } else { + file.path.should.equal('/test/test.coffee'); + file.history.should.eql(['/test/test.coffee']); + } + }); + + it('should strip trailing sep', function() { + var file = new File(); + file.path = '/test/'; + file.path.should.eql(path.normalize('/test')); + file.history.should.eql([path.normalize('/test')]); + + var file2 = new File({ + stat: { + isDirectory: function() { + return true; + }, + }, + }); + file2.path = '/test/'; + file2.path.should.eql(path.normalize('/test')); + file2.history.should.eql([path.normalize('/test')]); }); }); @@ -1025,7 +1291,7 @@ describe('File', function() { var file = new File({ symlink: '/test/test.coffee', }); - file.symlink.should.equal('/test/test.coffee'); + file.symlink.should.equal(path.normalize('/test/test.coffee')); done(); }); @@ -1042,15 +1308,27 @@ describe('File', function() { it('should set the symlink', function(done) { var file = new File(); file.symlink = '/test/test.coffee'; - file.symlink.should.equal('/test/test.coffee'); + file.symlink.should.equal(path.normalize('/test/test.coffee')); done(); }); it('should set the relative symlink', function(done) { var file = new File(); - file.symlink = './test.coffee'; - file.symlink.should.equal('./test.coffee'); + file.symlink = 'test.coffee'; + file.symlink.should.equal('test.coffee'); done(); }); + + it('should be normalized and stripped off a trailing sep on set', function() { + var file = new File(); + + file.symlink = '/test/foo/../bar/'; + + if (process.platform === 'win32') { + file.symlink.should.equal('\\test\\bar'); + } else { + file.symlink.should.equal('/test/bar'); + } + }); }); }); diff --git a/test/normalize.js b/test/normalize.js new file mode 100644 index 0000000..2bfa9a2 --- /dev/null +++ b/test/normalize.js @@ -0,0 +1,15 @@ +var normalize = require('../lib/normalize'); +var path = require('path'); +require('should'); +require('mocha'); + +describe('normalize()', function() { + it('should leave empty strings unmodified', function() { + normalize('').should.equal(''); + }); + + it('should apply path.normalize for everything else', function() { + var str = '/foo//../bar/baz'; + normalize(str).should.equal(path.normalize(str)); + }); +}); diff --git a/test/stripTrailingSep.js b/test/stripTrailingSep.js new file mode 100644 index 0000000..2a432ce --- /dev/null +++ b/test/stripTrailingSep.js @@ -0,0 +1,36 @@ +var stripTrailingSep = require('../lib/stripTrailingSep'); +require('should'); +require('mocha'); + +describe('stripTrailingSep()', function() { + it('should strip trailing separator', function() { + stripTrailingSep('foo/').should.equal('foo'); + stripTrailingSep('foo\\').should.equal('foo'); + }); + + it('should not strip when the only char in the string', function() { + stripTrailingSep('/').should.equal('/'); + stripTrailingSep('\\').should.equal('\\'); + }); + + it('should strip only the trailing separator', function() { + stripTrailingSep('/test/foo/bar/').should.equal('/test/foo/bar'); + stripTrailingSep('\\test\\foo\\bar\\').should.equal('\\test\\foo\\bar'); + }); + + it('should strip multiple trailing separators', function() { + stripTrailingSep('/test//').should.equal('/test'); + stripTrailingSep('\\test\\\\').should.equal('\\test'); + }); + + it('should leave 1st separator in a string of only separators', function() { + stripTrailingSep('//').should.equal('/'); + stripTrailingSep('////').should.equal('/'); + stripTrailingSep('\\\\').should.equal('\\'); + stripTrailingSep('\\\\\\\\').should.equal('\\'); + }); + + it('should return back empty string', function() { + stripTrailingSep('').should.equal(''); + }); +});