From 5a246f8e3c65323fe39753899ab8e8cc79de4605 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 24 Nov 2019 17:37:56 +0100 Subject: [PATCH] TypeScript refactoring --- .codeclimate.yml | 17 +- .eslintrc | 100 ++--- .gitignore | 7 - .prettierrc | 13 + .travis.yml | 1 - .vscode/extensions.json | 8 + .vscode/launch.json | 15 + .vscode/settings.json | 12 +- CHANGELOG.md | 4 + README.md | 511 ++++++++++++++--------- compress.js | 244 ----------- index.d.ts | 21 - index.js | 304 -------------- index.ts | 879 ++++++++++++++++++++++++++++++++++++++++ interval.js | 168 -------- package-lock.json | 429 +++++++++++++++----- package.json | 31 +- test/01rfs.js | 399 ------------------ test/01rfs.ts | 59 +++ test/02write.js | 152 ------- test/02write.ts | 108 +++++ test/03size.js | 168 -------- test/03size.ts | 62 +++ test/04errors.js | 718 -------------------------------- test/04errors.ts | 257 ++++++++++++ test/05options.js | 291 ------------- test/05options.ts | 137 +++++++ test/06interval.js | 261 ------------ test/06interval.ts | 125 ++++++ test/07compression.js | 558 ------------------------- test/07compression.ts | 117 ++++++ test/08classical.ts | 132 ++++++ test/08use_case.js | 201 --------- test/09classical.js | 432 -------------------- test/09history.ts | 174 ++++++++ test/10history.js | 377 ----------------- test/99clean.js | 9 - test/99clean.ts | 10 + test/helper.js | 70 ---- test/helper.ts | 145 +++++++ tsconfig.json | 8 + utils.js | 234 ----------- utils.ts | 52 +++ 43 files changed, 3032 insertions(+), 4988 deletions(-) delete mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json delete mode 100644 compress.js delete mode 100644 index.d.ts delete mode 100644 index.js create mode 100644 index.ts delete mode 100644 interval.js delete mode 100644 test/01rfs.js create mode 100644 test/01rfs.ts delete mode 100644 test/02write.js create mode 100644 test/02write.ts delete mode 100644 test/03size.js create mode 100644 test/03size.ts delete mode 100644 test/04errors.js create mode 100644 test/04errors.ts delete mode 100644 test/05options.js create mode 100644 test/05options.ts delete mode 100644 test/06interval.js create mode 100644 test/06interval.ts delete mode 100644 test/07compression.js create mode 100644 test/07compression.ts create mode 100644 test/08classical.ts delete mode 100644 test/08use_case.js delete mode 100644 test/09classical.js create mode 100644 test/09history.ts delete mode 100644 test/10history.js delete mode 100644 test/99clean.js create mode 100644 test/99clean.ts delete mode 100644 test/helper.js create mode 100644 test/helper.ts create mode 100644 tsconfig.json delete mode 100644 utils.js create mode 100644 utils.ts diff --git a/.codeclimate.yml b/.codeclimate.yml index af6cdb9..76e33f3 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,23 +1,30 @@ --- checks: + file-lines: + config: + threshold: 1000 method-complexity: config: threshold: 30 + method-count: + config: + threshold: 35 method-lines: config: threshold: 100 return-statements: config: threshold: 20 + similar-code: + config: + threshold: 60 engines: duplication: enabled: true config: languages: - - ruby - javascript - - python - - php + - typescript eslint: enabled: true channel: eslint-6 @@ -25,4 +32,6 @@ engines: enabled: true ratings: paths: - - "*.js" + - "*.ts" + - "test/*.ts" + - "test/*.js" diff --git a/.eslintrc b/.eslintrc index 9b49e51..81d4d1d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,8 @@ "jquery": true, "node": true }, + "extends": ["plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 9, "sourceType": "module" @@ -13,7 +15,7 @@ "rules": { "comma-dangle": [2, "only-multiline"], "no-cond-assign": 2, - "no-console": 0, + "no-console": 2, "no-constant-condition": 0, "no-control-regex": 2, "no-debugger": 2, @@ -43,7 +45,7 @@ "block-scoped-var": 0, "complexity": [2, 80], "consistent-return": 0, - "curly": ["error", "multi"], + "curly": ["error", "multi-or-nest"], "default-case": 0, "dot-location": 0, "dot-notation": 0, @@ -69,7 +71,7 @@ "no-lone-blocks": 2, "no-loop-func": 2, "no-magic-number": 0, - "no-multi-spaces": ["error", { "exceptions": { "ImportDeclaration": true, "Property": true, "VariableDeclarator": true } }], + "no-multi-spaces": 2, "no-multi-str": 0, "no-native-reassign": 2, "no-new-func": 2, @@ -117,65 +119,42 @@ "no-sync": 0, "array-bracket-spacing": 0, "block-spacing": "error", - "brace-style": [ - "error", - "stroustrup", - { - "allowSingleLine": true - } - ], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "camelcase": 0, - "comma-spacing": 0, + "comma-spacing": ["error", { "before": false, "after": true }], "comma-style": 0, "computed-property-spacing": 0, "consistent-this": 0, - "eol-last": 0, + "eol-last": 2, "func-names": 0, "func-style": 0, "id-length": 0, "id-match": 0, "indent": [2, "tab"], "jsx-quotes": 0, - "key-spacing": [ - "error", - { - "align": { - "beforeColon": false, - "afterColon": true, - "on": "value" - } - } - ], + "key-spacing": ["error", { "align": { "beforeColon": false, "afterColon": true, "on": "value" } }], "keyword-spacing": [ "error", { + "before": true, "overrides": { - "catch": { - "after": false - }, - "if": { - "after": false - }, - "for": { - "after": false - }, - "switch": { - "after": false - }, - "while": { - "after": false - } + "catch": { "after": false }, + "if": { "after": false }, + "for": { "after": false }, + "switch": { "after": false }, + "while": { "after": false } } } ], "linebreak-style": 0, "lines-around-comment": 0, "max-depth": 0, - "max-len": 0, - "max-lines": ["error", 750], + "max-len": [2, { "code": 200 }], + "max-lines": ["error", 1000], "max-nested-callbacks": 0, "max-params": 0, "max-statements": ["error", 150], + "max-statements-per-line": [2, { "max": 1 }], "new-cap": 0, "new-parens": 0, "newline-after-var": 0, @@ -184,49 +163,44 @@ "no-continue": 0, "no-inline-comments": 0, "no-lonely-if": "error", - "no-mixed-spaces-and-tabs": 0, - "no-multiple-empty-lines": 0, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": 2, "no-negated-condition": 0, "no-nested-ternary": 0, "no-new-object": 0, "no-plusplus": 0, "no-restricted-syntax": 0, "no-spaced-func": 0, + "no-tabs": ["error", { "allowIndentationTabs": true }], "no-ternary": 0, - "no-trailing-spaces": 0, + "no-trailing-spaces": 2, "no-underscore-dangle": 0, "no-unneeded-ternary": 0, - "object-curly-spacing": 0, + "no-whitespace-before-property": 2, + "nonblock-statement-body-position": ["error", "beside"], + "object-curly-spacing": [2, "always"], "one-var": 0, + "one-var-declaration-per-line": 0, "operator-assignment": 0, "operator-linebreak": 0, "padded-blocks": ["error", "never"], "quote-props": 0, "quotes": ["error", "double"], "require-jsdoc": 0, - "semi-spacing": 0, "semi": 2, + "semi-spacing": 2, + "sort-imports": 2, "sort-vars": 0, - "space-before-blocks": 0, + "space-before-blocks": [2, "always"], "space-before-function-paren": 0, - "space-in-parens": 0, - "space-infix-ops": 0, - "space-unary-ops": [ - "error", - { - "words": true, - "nonwords": false, - "overrides": { - "!": true, - "typeof": false - } - } - ], + "space-in-parens": ["error", "never"], + "space-infix-ops": 2, + "space-unary-ops": ["error", { "words": true, "nonwords": false, "overrides": { "!": true } }], "spaced-comment": 0, "wrap-regex": 0, - "arrow-body-style": ["error", "as-needed"], - "arrow-parens": 0, - "arrow-spacing": 0, + "arrow-body-style": [2, "as-needed"], + "arrow-parens": [2, "as-needed"], + "arrow-spacing": 2, "constructor-super": 0, "generator-star-spacing": 0, "no-arrow-condition": 0, @@ -241,6 +215,8 @@ "prefer-reflect": 0, "prefer-spread": 0, "prefer-template": 0, - "require-yield": 0 + "require-yield": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/type-annotation-spacing": [2, { "before": false, "after": true, "overrides": { "arrow": { "before": true } } }] } } diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0aac560..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*gz -*log -*tmp -.npmignore -.nyc_output -coverage -node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6f36e7a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "jsxBracketSameLine": true, + "printWidth": 200, + "useTabs": true, + "overrides": [ + { + "files": ["*.md"], + "options": { + "useTabs": false + } + } + ] +} diff --git a/.travis.yml b/.travis.yml index b39753d..ebbc0d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ language: node_js node_js: - "12" - "10" - - "8" after_script: - if [[ `node --version` =~ ^v12 ]] ; then npm run coverage ; npm install codeclimate-test-reporter ; codeclimate-test-reporter < coverage/lcov.info ; fi diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..8434437 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "eg2.vscode-npm-script", + "myh.preview-vscode", + "prettier.prettier-vscode" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b39103b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha All", + "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", + "args": ["-r", "ts-node/register", "--timeout", "999999", "--colors", "${workspaceFolder}/test/*.ts"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "protocol": "inspector" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 5449a9a..ed768ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,17 @@ { + "editor.defaultFormatter": "prettier.prettier-vscode", "editor.formatOnSave": true, "eslint.autoFixOnSave": true, "eslint.alwaysShowStatus": true, + "eslint.validate": ["javascript", { "language": "typescript", "autoFix": true }], "explorer.confirmDelete": false, + "files.autoSave": "off", "files.eol": "\n", "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "git.ignoreMissingGitWarning": true, - "prettier.eslintIntegration": true, - "prettier.jsxBracketSameLine": true, - "prettier.printWidth": 200, - "prettier.useTabs": true, - "window.zoomLevel": 0 + "window.zoomLevel": 0, + "[typescript]": { + "editor.defaultFormatter": "prettier.prettier-vscode" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index c13a423..8e61700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +- 2019-11-24 - v2.0.0 + - complete refactoring with TypeScript + - full Windows compliance (at least all tests are OK) + - file is recreated if externally removed while logging - 2019-10-20 - v1.4.6 - tests fix - 2019-10-18 - v1.4.5 diff --git a/README.md b/README.md index 88da348..2f2b01c 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,21 @@ [![Dependencies](https://david-dm.org/iccicci/rotating-file-stream.svg)](https://david-dm.org/iccicci/rotating-file-stream) [![Dev Dependencies](https://david-dm.org/iccicci/rotating-file-stream/dev-status.svg)](https://david-dm.org/iccicci/rotating-file-stream?type=dev) -[![NPM](https://nodei.co/npm/rotating-file-stream.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/rotating-file-stream/) +[![NPM](https://nodei.co/npm/rotating-file-stream.png?downloads=true&downloadRank=true)](https://nodei.co/npm/rotating-file-stream/) ### Description -Creates a [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable) to a file which is rotated. -Rotation behaviour can be deeply customized; optionally, classical UNIX **logrotate** behaviour can be used. +Creates a [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable) to a file which is +rotated. Rotation behaviour can be deeply customized; optionally, classical UNIX **logrotate** behaviour can be used. ### Usage ```javascript -var rfs = require("rotating-file-stream"); -var stream = rfs("file.log", { - size: "10M", // rotate every 10 MegaBytes written - interval: "1d", // rotate daily - compress: "gzip" // compress rotated files +const rfs = require("rotating-file-stream"); +const stream = rfs.createStream("file.log", { + size: "10M", // rotate every 10 MegaBytes written + interval: "1d", // rotate daily + compress: "gzip" // compress rotated files }); ``` @@ -37,132 +37,251 @@ $ npm install --save rotating-file-stream ### Table of contents +- [Upgrading from v1.x.x to v2.x.x](#upgrading-from-v1xx-to-v2xx) - [API](#api) + - [rfs.createStream(filename[, options])](#rfscreatestreamfilename-options) + - [filename](#filename) + - [filename(time[, index])](#filenametime-index) + - [filename(index)](#filenameindex) - [Class: RotatingFileStream](#class-rotatingfilestream) - - [RotatingFileStream(filename, options)](#new-rotatingfilestreamfilename-options) - - [filename](#filename-stringfunction) - - [options](#options-object) - - [compress](#compress) - - [history](#history) - - [immutable](#immutable) - - [initialRotation](#initialrotation) - - [interval](#interval) - - [maxFiles](#maxfiles) - - [maxSize](#maxsize) - - [path](#path) - - [rotate](#rotate) - - [rotationTime](#rotationtime) - - [size](#size) - - [Events](#events) - - [Rotation logic](#rotation-logic) - - [Under the hood](#under-the-hood) - - [Compatibility](#compatibility) - - [TypeScript](#typescript) - - [Licence](#licence) - - [Bugs](#bugs) - - [ChangeLog](#changelog) - - [Donating](#donating) + - [Event: 'history'](#event-history) + - [Event: 'open'](#event-open) + - [Event: 'removed'](#event-removed) + - [Event: 'rotation'](#event-rotation) + - [Event: 'rotated'](#event-rotated) + - [Event: 'warning'](#event-warning) + - [options](#options) + - [compress](#compress) + - [encoding](#encoding) + - [history](#history) + - [immutable](#immutable) + - [initialRotation](#initialrotation) + - [interval](#interval) + - [intervalBoundary](#intervalboundary) + - [maxFiles](#maxfiles) + - [maxSize](#maxsize) + - [mode](#mode) + - [path](#path) + - [rotate](#rotate) + - [size](#size) +- [Rotation logic](#rotation-logic) +- [Under the hood](#under-the-hood) +- [Compatibility](#compatibility) +- [TypeScript](#typescript) +- [Licence](#licence) +- [Bugs](#bugs) +- [ChangeLog](#changelog) +- [Donating](#donating) + +# Upgrading from v1.x.x to v2.x.x + +There are two main changes in package interface. + +In **v1** the _default export_ of the packege was directly the **RotatingFileStream** _constructor_ and the caller +have to use it; while in **v2** there is no _default export_ and the caller should use the +[createStream](#rfscreatestreamfilename-options) exported function and should not directly use +[RotatingFileStream](#class-rotatingfilestream) class. +This is quite easy to discover: if this change is not applied, nothing than a runtime error can happen. + +The other important change is the removal of option **rotationTime** and the introduction of **intervalBoundary**. +In **v1** the `time` argument passed to the _filename generator_ function, by default, is the time when _rotaion job_ +started, while if [`options.interval`](#interval) option is used, it is the lower boundary of the time interval within +_rotaion job_ started. Later I was asked to add the possibility to restore the default value for this argument so I +introduced `options.rotationTime` option with this purpose. At the end the result was something a bit confusing, +something I never liked. +In **v2** the `time` argument passed to the _filename generator_ function is always the time when _rotaion job_ +started, unless [`options.intervalBoundary`](#intervalboundary) option is used. In a few words, to maintain back compatibility +upgrading from **v1** to **v2**, just follow this rules: + +- using [`options.rotation`](#rotation): nothing to do +- not using [`options.rotation`](#rotation): + - not using [`options.interval`](#interval): nothing to do + - using [`options.interval`](#interval): + - using `options.rotationTime`: to remove it + - not using `options.rotationTime`: then use [`options.intervalBoundary`](#intervalboundary). # API ```javascript -require("rotating-file-stream"); +const rfs = require("rotating-file-stream"); ``` -Returns **RotatingFileStream** constructor. - -## Class: RotatingFileStream - -Extends [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable). +## rfs.createStream(filename[, options]) -## [new] RotatingFileStream(filename, options) +- `filename` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | + [<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) The name + of the file or the function to generate it, called _file name generator_. See below for + [details](#filename-stringfunction). +- `options` [<Object>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) + Rotation options, See below for [details](#options). +- Returns: [<RotatingFileStream>](#class-rotatingfilestream) The **rotating file stream**! -Returns a new **RotatingFileStream** to _filename_ as -[fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) does. -The file is rotated following _options_ rules. +This interface is inspired to +[fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) one. The file is rotated +following _options_ rules. -### filename {String|Function} +### filename The most complex problem about file name is: "how to call the rotated file name?" The answer to this question may vary in many forms depending on application requirements and/or specifications. -If there are no requirements, a _String_ can be used and _default rotated file name generator_ will be used; -otherwise a _Function_ which returns the _rotated file name_ can be used. +If there are no requirements, a `string` can be used and _default rotated file name generator_ will be used; +otherwise a `Function` which returns the _rotated file name_ can be used. -#### function filename(time, index) +**Note:** +if part of returned destination path does not exists, the rotation job will try to create it. -- time: {Date} If both rotation by interval is enabled and **options.rotationTime** [(see below)](#rotationtime) is - **false**, the start time of rotation period, otherwise the time when rotation job started. If **null**, the - _not-rotated file name_ must be returned. -- index {Number} The progressive index of rotation by size in the same rotation period. +#### filename(time[, index]) -An example of a complex _rotated file name generator_ function could be: +- `time` [<Date>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) -```javascript -function pad(num) { - return (num > 9 ? "" : "0") + num; -} + - By default: the time when rotation job started; + - if both [`options.interval`](#interval) and [`intervalBoundary`](#intervalboundary) options are enabled: the start + time of rotation period. -function generator(time, index) { - if (!time) return "file.log"; + If `null`, the _not-rotated file name_ must be returned. - var month = time.getFullYear() + "" + pad(time.getMonth() + 1); - var day = pad(time.getDate()); - var hour = pad(time.getHours()); - var minute = pad(time.getMinutes()); +- `index` [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The + progressive index of rotation by size in the same rotation period. - return month + "/" + month + day + "-" + hour + minute + "-" + index + "-file.log"; -} +An example of a complex _rotated file name generator_ function could be: -var rfs = require("rotating-file-stream"); -var stream = rfs(generator, { - size: "10M", - interval: "30m" +```javascript +const pad = num => (num > 9 ? "" : "0") + num; +const generator = (time, index) => { + if (!time) return "file.log"; + + var month = time.getFullYear() + "" + pad(time.getMonth() + 1); + var day = pad(time.getDate()); + var hour = pad(time.getHours()); + var minute = pad(time.getMinutes()); + + return `${month}/${month}${day}-${hour}${minute}-${index}-file.log`; +}; + +const rfs = require("rotating-file-stream"); +const stream = rfs(generator, { + size: "10M", + interval: "30m" }); ``` **Note:** -if both rotation by interval and rotation by time are used, returned _rotated file name_ **must** be function of both -parameters _time_ and _index_. Alternatively, **rotationTime** _option_ can be used (to see below). +if all of [`options.interval`](#interval), [`options.size`](#size) and [`options.intervalBoundary`](#intervalBoundary) +are used, returned _rotated file name_ **must** be function of both arguments `time` and `index`. -If classical **logrotate** behaviour is enabled _rotated file name_ is only a function of _index_. +#### filename(index) -#### function filename(index) +- `index` [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The + progressive index of rotation. If `null`, the _not-rotated file name_ must be returned. -- index {Number} The progressive index of rotation. If **null**, the _not-rotated file name_ must be returned. +If classical **logrotate** behaviour is enabled (by [`options.rotate`](#rotate)), _rotated file name_ is only a +function of `index`. -**Note:** -The _not-rotated file name_ **must** be only the _filename_, to specify a _path_ the appropriate option **must** be used. +## Class: RotatingFileStream -```javascript -rfs("path/to/file.log"); // wrong -rfs("file.log", { path: "path/to" }); // OK -``` +Extends [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable). It should not be directly +used. Exported only to be used with `instanceof` operator and similar. -**Note:** -if part of returned destination path does not exists, the rotation job will try to create it. +### Event: 'history' + +The `history` event is emitted once the _history check job _ is completed. + +### Event: 'open' + +- `filename` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) Is + constant unless [`options.immutable`](#immutable) is `true`. + +The `open` event is emitted once the _not-rotated file_ is opened. + +### Event: 'removed' + +- `filename` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The + name of the removed file. +- `number` [<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) + - `true` if the file was removed due to [`options.maxFiles`](#maxFiles) + - `false` if the file was removed due to [`options.maxSize`](#maxSize) + +The `removed` event is emitted once a _rotated file_ is removed due to [`options.maxFiles`](#maxFiles) or +[`options.maxSize`](#maxSize). + +### Event: 'rotation' + +The `rotation` event is emitted once the _rotation job_ is started. + +### Event: 'rotated' + +- `filename` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The + _rotated file name_ produced. + +The `rotated` event is emitted once the _rotation job_ is completed. + +### Event: 'warning' + +- `error` [<Error>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) The + non blocking error. + +The `warning` event is emitted once a non blocking error happens. + +## options + +- [`compress`](#compress): + [<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) | + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | + [<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) + Specifies compression method of rotated files. **Default:** `null`. +- [`encoding`](#encoding): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the default encoding. **Default:** `'utf8'`. +- [`history`](#history): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the _history filename_. **Default:** `null`. +- [`immutable`](#immutable): + [<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) + Never mutate file names. **Default:** `null`. +- [`initialRotation`](#initialRotation): + [<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) + Initial rotation based on _not-rotated file_ timestamp. **Default:** `null`. +- [`interval`](#interval): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the time interval to rotate the file. **Default:** `null`. +- [`intervalBoundary`](#intervalBoundary): + [<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) + Makes rotated file name with lower boundary of rotation period. **Default:** `null`. +- [`maxFiles`](#maxFiles): + [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) + Specifies the maximum number of rotated files to keep. **Default:** `null`. +- [`maxSize`](#maxSize): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the maximum size of rotated files to keep. **Default:** `null`. +- [`mode`](#mode): + [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) + Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options). + **Default:** `0o666`. +- [`path`](#path): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the base path for files. **Default:** `null`. +- [`rotate`](#rotate): + [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) + Enables the classical UNIX **logrotate** behaviour. **Default:** `null`. +- [`size`](#size): + [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) + Specifies the file size to rotate the file. **Default:** `null`. + +### encoding + +Specifies the default encoding that is used when no encoding is specified as an argument to +[stream.write()](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback). -### options {Object} +### mode -- compress: {String|Function|True} (default: null) Specifies compression method of rotated files. -- highWaterMark: {Number} (default: null) Proxied to [new stream.Writable](https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options) -- history: {String} (default: null) Specifies the _history filename_. -- immutable: {Boolean} (default: null) Never mutates file names. -- initialRotation: {Boolean} (default: null) Initial rotation based on _not-rotated file_ timestamp. -- interval: {String} (default: null) Specifies the time interval to rotate the file. -- maxFiles: {Integer} (default: null) Specifies the maximum number of rotated files to keep. -- maxSize: {String} (default: null) Specifies the maximum size of rotated files to keep. -- mode: {Integer} (default: null) Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) -- path: {String} (default: null) Specifies the base path for files. -- rotate: {Integer} (default: null) Enables the classical UNIX **logrotate** behaviour. -- rotationTime: {Boolean} (default: null) Makes rotated file name with time of rotation. -- size: {String} (default: null) Specifies the file size to rotate the file. +Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options). -#### path +### path If present, it is prepended to generated file names as well as for history file. -#### size +### size Accepts a positive integer followed by one of these possible letters: @@ -188,7 +307,7 @@ Accepts a positive integer followed by one of these possible letters: size: '1G', // rotates the file when size exceeds a GigaByte ``` -#### interval +### interval Accepts a positive integer followed by one of these possible letters: @@ -219,161 +338,133 @@ Accepts a positive integer followed by one of these possible letters: interval: '1M', // rotates at every midnight between two distinct months ``` -#### compress +### intervalBoundary -Due the nature of **Node.js** compression may be done with an external command (to use other CPUs than the one used -by **Node.js**) or with internal code (to use the CPU used by **Node.js**). This decision is left to you. +If set to `true`, the argument `time` of _filename generator_ is no longer the time when _rotation job_ started, but +the _lower boundary_ of rotation interval. -Following fixed strings are allowed to compress the files with internal libraries: +**Note:** +this option has effec only if [`options.interval`](#interval) is used. + +### initialRotation + +When program stops in a rotation period then restarts in a new rotation period, logs of different rotation period will +go in the next rotated file; in a few words: a rotation job is lost. If this option is set to `true` an initial check +is performed against the _not-rotated file_ timestamp and, if it falls in a previous rotation period, an initial +rotation job is done as well. + +**Note:** +this option has effec only if [`options.intervalBoundary`](#intervalboundary) is used. -- bzip2 (**not implemented yet**) -- gzip +### compress -To enable external compression, a _function_ can be used or simply the _boolean_ **true** value to use default +For historical reasons external compression can be used, but the best choice is to use the value `"gzip"`. + +To enable external compression, a _function_ can be used or simply the _boolean_ `true` value to use default external compression. -The function should accept _source_ and _dest_ file names and must return the shell command to be executed to +The function should accept `source` and `dest` file names and must return the shell command to be executed to compress the file. The two following code snippets have exactly the same effect: ```javascript var rfs = require("rotating-file-stream"); var stream = rfs("file.log", { - size: "10M", - compress: true + size: "10M", + compress: true }); ``` ```javascript var rfs = require("rotating-file-stream"); var stream = rfs("file.log", { - size: "10M", - compress: function(source, dest) { - return "cat " + source + " | gzip -c9 > " + dest; - } + size: "10M", + compress: (source, dest) => "cat " + source + " | gzip -c9 > " + dest }); ``` **Note:** -this option is ignored if **immutable** is set to **true**. +this option is ignored if [`options.immutable`](#immutable) is used. **Note:** the shell command to compress the rotated file should not remove the source file, it will be removed by the package if rotation job complete with success. -#### initialRotation +### initialRotation When program stops in a rotation period then restarts in a new rotation period, logs of different rotation period will -go in the next rotated file; in a few words: a rotation job is lost. If this option is set to **true** an initial check +go in the next rotated file; in a few words: a rotation job is lost. If this option is set to `true` an initial check is performed against the _not-rotated file_ timestamp and, if it falls in a previous rotation period, an initial rotation job is done as well. **Note:** -this option is ignored if **rotationTime** is set to **true**. +this option has effect only if both [`options.interval`](#interval) and [`options.intervalBoundary`](#intervalboundary) +are used. -#### rotate +**Note:** +this option is ignored if [`options.rotate`](#rotate) is used. + +### rotate If specified, classical UNIX **logrotate** behaviour is enabled and the value of this option has same effect in _logrotate.conf_ file. **Note:** -following options are ignored if **rotate** option is specified. +if this optoin is used following ones take no effect: [`options.history`](#history), [`options.immutable`](#immutable), +[`options.initialRotation`](#initialrotation), [`options.intervalBoundary`](#intervalboundary), +[`options.maxFiles`](#maxfiles), [`options.maxSize`](#maxsize). -#### immutable +### immutable -If set to **true**, names of generated files never changes. New files are immediately generated with their rotated -name. In other words the _rotated file name generator_ is never called with a **null** _time_ parameter unless to -determinate the _history file_ name; this can happen if **maxFiles** or **maxSize** are used without **history** -option. **rotation** _event_ now has a _filename_ parameter with the newly created file name. +If set to `true`, names of generated files never changes. New files are immediately generated with their rotated +name. In other words the _rotated file name generator_ is never called with a `null` _time_ argument unless to +determinate the _history file_ name; this can happen if [`options.history`](#history) is not used while +[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are used. +The `filename` argument passed to [`'open'`](#event-open) _event_ evaluates now as the newly created file name. Useful to send logs to logstash through filebeat. **Note:** -if this option is set to **true**, **compress** is ignored. - -**Note:** -this option is ignored if **interval** is not set. - -#### rotationTime - -As specified above, if rotation by interval is enabled, the parameter _time_ passed to _rotated file name generator_ is the -start time of rotation period. Setting this option to **true**, parameter _time_ passed is time when rotation job -started. +if this option is used, [`options.compress`](#compress) is ignored. **Note:** -if this option is set to **true**, **initialRotation** is ignored. +this option is ignored if [`options.interval`](#interval) is not used. -#### history +### history Due to the complexity that _rotated file names_ can have because of the _filename generator function_, if number or size of rotated files should not exceed a given limit, the package needs a file where to store this information. This -option specifies the name _history file_. This option takes effect only if at least one of **maxFiles** or **maxSize** -is used. If **null**, the _not rotated filename_ with the '.txt' suffix is used. +option specifies the name _history file_. This option takes effect only if at least one of +[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) is used. If `null`, the _not rotated filename_ with +the `'.txt'` suffix is used. -#### maxFiles +### maxFiles If specified, it's value is the maximum number of _rotated files_ to be kept. -#### maxSize - -If specified, it's value must respect same syntax of [size](#size) option and is the maximum size of _rotated files_ -to be kept. - -## Events - -Custom _Events_ are emitted by the stream. - -```javascript -var rfs = require('rotating-file-stream'); -var stream = rfs(...); - -stream.on('error', function(err) { - // here are reported blocking errors - // once this event is emitted, the stream will be closed as well -}); - -stream.on('open', function(filename) { - // no rotated file is open (emitted after each rotation as well) - // filename: useful if immutable option is true -}); +### maxSize -stream.on('removed', function(filename, number) { - // rotation job removed the specified old rotated file - // number == true, the file was removed to not exceed maxFiles - // number == false, the file was removed to not exceed maxSize -}); +If specified, it's value must respect same syntax of [option.size](#size) and is the maximum size of _rotated files_ to +be kept. -stream.on('rotation', function() { - // rotation job started -}); - -stream.on('rotated', function(filename) { - // rotation job completed with success producing given filename -}); - -stream.on('warning', function(err) { - // here are reported non blocking errors -}); -``` - -## Rotation logic +# Rotation logic Regardless of when and why rotation happens, the content of a single [stream.write](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback) will never be split among two files. -### by size +## by size Once the _not-rotated_ file is opened first time, its size is checked and if it is greater or equal to size limit, a first rotation happens. After each [stream.write](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback), the same check is performed. -### by interval +## by interval The package sets a [Timeout](https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args) to start a rotation job at the right moment. -## Under the hood +# Under the hood Logs should be handled so carefully, so this package tries to never overwrite files. @@ -383,45 +474,73 @@ an initial rotation attempt is done. At each rotation attempt a check is done to verify that destination rotated file does not exists yet; if this is not the case a new destination _rotated file name_ is generated and the same check is performed before going on. This is repeated until a not existing destination file name is found or the -package is exhausted. For this reason the _rotated file name generator_ function may be called several +package is exhausted. For this reason the _rotated file name generator_ function could be called several times for each rotation job. -If requested by **maxFiles** or **maxSize** options, at the end of a rotation job, a check is performed to ensure that -given limits are respected. This means that **while rotation job is running both the limits could be not respected**, -the same can happen (if **maxFiles** or **maxSize** are changed) till the end of first _rotation job_. -The first check performed is the one against **maxFiles**, in case some files are removed, then the check against -**maxSize** is performed, finally other files can be removed. When **maxFiles** or **maxSize** are enabled for first -time, an _history file_ can be created with one _rotated filename_ (as returned by _filename generator function_) at -each line. +If requested through [`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize), at the end of a rotation job, a +check is performed to ensure that given limits are respected. This means that +**while rotation job is running both the limits could be not respected**. The same can happen till the end of first +rotation job* if [`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are changed between two runs. +The first check performed is the one against [`options.maxFiles`](#maxfiles), in case some files are removed, then the +check against [`options.maxSize`](#maxsize) is performed, finally other files can be removed. When +[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are enabled for first time, an \_history file* can be +created with one _rotated filename_ (as returned by _filename generator function_) at each line. Once an **error** _event_ is emitted, nothing more can be done: the stream is closed as well. -## Compatibility +# Compatibility + +Requires **Node.js v10.x**. The package is tested under [all Node.js versions](https://travis-ci.org/iccicci/rotating-file-stream) currently supported accordingly to [Node.js Release](https://github.com/nodejs/Release). -## TypeScript +To work with the package under Windows, be sure to configure `bash.exe` as your _script-shell_. -To import the package in a **TypeScript** project, use following import statement. +``` +> npm config set script-shell bash.exe +``` + +# TypeScript + +Exported in **TypeScript**. ```typescript -import rfs from "rotating-file-stream"; +import { Writable } from "stream"; +export declare type Compressor = (source: string, dest: string) => string; +export declare type Generator = (time: number | Date, index?: number) => string; +export interface Options { + compress?: boolean | string | Compressor; + encoding?: string; + history?: string; + immutable?: boolean; + initialRotation?: boolean; + interval?: string; + intervalBoundary?: boolean; + maxFiles?: number; + maxSize?: string; + mode?: number; + path?: string; + rotate?: number; + size?: string; +} +export declare class RotatingFileStream extends Writable {} +export declare function createStream(filename: string | Generator, options?: Options): RotatingFileStream; ``` -## Licence +# Licence [MIT Licence](https://github.com/iccicci/rotating-file-stream/blob/master/LICENSE) -## Bugs +# Bugs Do not hesitate to report any bug or inconsistency [@github](https://github.com/iccicci/rotating-file-stream/issues). -## ChangeLog +# ChangeLog [ChangeLog](https://github.com/iccicci/rotating-file-stream/blob/master/CHANGELOG.md) -## Donating +# Donating If you find useful this package, please consider the opportunity to donate some satoshis to this bitcoin address: **12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN** diff --git a/compress.js b/compress.js deleted file mode 100644 index b250023..0000000 --- a/compress.js +++ /dev/null @@ -1,244 +0,0 @@ -"use strict"; - -var cp = require("child_process"); -var fs = require("fs"); -var path = require("path"); -var utils = require("./utils"); -var zlib = require("zlib"); - -function classical(count) { - var prevName; - var thisName; - var self = this; - - if(this.options.rotate === count) delete this.rotatedName; - - var callback = function(err) { - if(err) return self.emit("error", err); - - self.open(); - - if(self.options.compress) self.compress(thisName); - else { - self.emit("rotated", self.rotatedName); - self.interval(); - } - }; - - try { - prevName = count === 1 ? this.name : this.generator(count - 1); - thisName = this.generator(count); - } - catch(e) { - return callback(e); - } - - var doIt = function(done) { - fs.rename(prevName, thisName, function(err) { - if(err) { - if(err.code !== "ENOENT") return callback(err); - - return utils.makePath(thisName, function(err) { - if(err) return callback(err); - - fs.rename(prevName, thisName, function(err) { - if(err) return callback(err); - - process.nextTick(done); - }); - }); - } - - process.nextTick(done); - }); - }; - - fs.stat(prevName, function(err) { - if(! err) { - if(! self.rotatedName) self.rotatedName = thisName; - - if(count !== 1) return doIt(self.classical.bind(self, count - 1)); - - if(self.options.compress) - return self.findName({}, true, function(err, name) { - if(err) return callback(err); - - thisName = name; - doIt(callback); - }); - - return doIt(callback); - } - - if(err.code !== "ENOENT") return callback(err); - - self.classical(count - 1); - }); -} - -function compress(tmp) { - var self = this; - - this.findName({}, false, function(err, name) { - if(err) return self.emit("error", err); - - self.touch(name, function(err) { - if(err) return self.emit("error", err); - - var done = function(err) { - if(err) return self.emit("error", err); - - fs.unlink(tmp, function(err) { - if(err) self.emit("warning", err); - - if(self.options.rotate) self.emit("rotated", self.rotatedName); - else self.emit("rotated", name); - - self.interval(); - }); - }; - - if(typeof self.options.compress === "function") self.external(tmp, name, done); - else self.gzip(tmp, name, done); - /* - if(self.options.compress == "gzip") - self.gzip(tmp, name, done); - else - throw new Error("Not implemented yet"); - */ - }); - }); -} - -function external(src, dst, callback) { - var att = {}; - var cont; - var self = this; - - try { - cont = self.options.compress(src, dst); - } - catch(e) { - return process.nextTick(callback.bind(null, e)); - } - - att[dst] = 1; - self.findName(att, true, function(err, name) { - if(err) return callback(err); - - fs.open(name, "w", parseInt("777", 8), function(err, fd) { - if(err) return callback(err); - - var unlink = function(err) { - fs.unlink(name, function(err2) { - if(err2) self.emit("warning", err2); - - callback(err); - }); - }; - - fs.write(fd, cont, function(err) { - fs.close(fd, function(err2) { - if(err) { - if(err2) self.emit("warning", err2); - - return unlink(err); - } - - if(err2) return unlink(err2); - - if(name.indexOf(path.sep) === -1) name = "." + path.sep + name; - - cp.exec(name, unlink); - }); - }); - }); - }); -} - -function exhausted(attempts) { - var err = new Error("Too many destination file attempts"); - err.code = "RFS-TOO-MANY"; - - if(attempts) err.attempts = attempts; - - return err; -} - -function findName(attempts, tmp, callback) { - var count = 0; - - for(var i in attempts) count += attempts[i]; - - if(count >= 1000) return callback(this.exhausted(attempts)); - - var name = this.name + "." + count + ".rfs.tmp"; - var self = this; - - if(! tmp) - try { - var pars = [count + 1]; - - if(! this.options.rotate) - if(this.options.interval && ! this.options.rotationTime) pars.unshift(new Date(this.prev)); - else pars.unshift(this.rotation); - - name = this.generator.apply(this, pars); - } - catch(e) { - return process.nextTick(callback.bind(null, e)); - } - - if(name in attempts) { - attempts[name]++; - - return self.findName(attempts, tmp, callback); - } - - fs.stat(name, function(err) { - if(! err || err.code !== "ENOENT") { - attempts[name] = 1; - - return self.findName(attempts, tmp, callback); - } - - callback(null, name); - }); -} - -function gzip(src, dst, callback) { - const inp = fs.createReadStream(src); - const out = fs.createWriteStream(dst); - const zip = zlib.createGzip(); - - [inp, out, zip].map(e => e.once("error", callback)); - out.once("finish", callback); - - inp.pipe(zip).pipe(out); -} - -function touch(name, callback, retry) { - var self = this; - - fs.open(name, "a", function(err, fd) { - if(err && err.code !== "ENOENT" && ! retry) return callback(err); - - if(! err) return fs.close(fd, callback); - - utils.makePath(name, function(err) { - if(err) return callback(err); - - self.touch(name, callback, true); - }); - }); -} - -module.exports = { - classical: classical, - compress: compress, - exhausted: exhausted, - external: external, - findName: findName, - gzip: gzip, - touch: touch -}; diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 82687bd..0000000 --- a/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { WriteStream } from "fs"; - -export interface RfsOptions { - compress?: string | Function | boolean; - highWaterMark?: number; - history?: string; - immutable?: boolean; - initialRotation?: boolean; - interval?: string; - maxFiles?: number; - maxSize?: string; - mode?: number; - path?: string; - rotate?: number; - rotationTime?: boolean; - size?: string; -} - -declare function RotatingFileStream(fileName: string | Function, options: RfsOptions): WriteStream; - -export default RotatingFileStream; diff --git a/index.js b/index.js deleted file mode 100644 index 5340209..0000000 --- a/index.js +++ /dev/null @@ -1,304 +0,0 @@ -"use strict"; - -var compress = require("./compress"); -var fs = require("fs"); -var interval = require("./interval"); -var path = require("path"); -var util = require("util"); -var utils = require("./utils"); -var Writable = require("stream").Writable; - -function RotatingFileStream(filename, options) { - if(! (this instanceof RotatingFileStream)) return new RotatingFileStream(filename, options); - - options = utils.checkOptions(options); - - if(typeof filename === "function") this.generator = filename; - else if(typeof filename === "string") - if(options.rotate) this.generator = utils.createClassical(filename); - else this.generator = utils.createGenerator(filename); - else throw new Error("Don't know how to handle 'filename' type: " + typeof filename); - - if(options.path) { - var generator = this.generator; - - this.generator = function(time, index) { - return path.join(options.path, generator(time, index)); - }; - } - - var opt = {}; - - if(options.highWaterMark) opt.highWaterMark = options.highWaterMark; - - if(options.mode) opt.mode = options.mode; - - Writable.call(this, opt); - - this.chunks = []; - this.options = options; - this.size = 0; - this.write = this.write; // https://github.com/iccicci/rotating-file-stream/issues/19 - - utils.setEvents(this); - - process.nextTick(this.firstOpen.bind(this)); -} - -util.inherits(RotatingFileStream, Writable); - -RotatingFileStream.prototype._close = function(done) { - if(this.stream) { - this.stream.on("finish", done); - this.stream.end(); - this.stream = null; - } - else done(); -}; - -RotatingFileStream.prototype._rewrite = function() { - const self = this; - const callback = function() { - if(self.ending) self._close(Writable.prototype.end.bind(self)); - }; - - if(this.err) { - const chunks = this.chunks; - - this.chunks = []; - chunks.map(e => { - if(e.cb) e.cb(); - }); - - return callback(); - } - - if(this.writing || this.rotation) return; - if(this.options.size && this.size >= this.options.size) return this.rotate(); - if(! this.stream) return; - if(! this.chunks.length) return callback(); - - const chunk = this.chunks[0]; - - this.chunks.shift(); - this.size += chunk.chunk.length; - this.writing = true; - - this.stream.write(chunk.chunk, function(err) { - self.writing = false; - - if(err) self.emit("error", err); - - if(chunk.cb) chunk.cb(); - - process.nextTick(self._rewrite.bind(self)); - }); -}; - -RotatingFileStream.prototype._write = function(chunk, encoding, callback) { - this.chunks.push({ chunk: chunk, cb: callback }); - this._rewrite(); -}; - -RotatingFileStream.prototype._writev = function(chunks, callback) { - chunks[chunks.length - 1].cb = callback; - this.chunks = this.chunks.concat(chunks); - this._rewrite(); -}; - -RotatingFileStream.prototype.end = function() { - var args = []; - - for(var i = 0; i < arguments.length; ++i) { - if("function" === typeof arguments[i]) { - this.once("finish", arguments[i]); - - break; - } - - if(i > 1) break; - - args.push(arguments[i]); - } - - this.ending = true; - - if(args.length) this.write.apply(this, args); - else this._rewrite(); -}; - -RotatingFileStream.prototype.firstOpen = function() { - var self = this; - - if(this.options.immutable) return this.immutate(true); - - try { - this.name = this.generator(null); - } - catch(e) { - return this.emit("error", e); - } - - this.once("open", this.interval.bind(this)); - - fs.stat(this.name, function(err, stats) { - if(err) { - if(err.code === "ENOENT") return self.open(); - - return self.emit("error", err); - } - - if(! stats.isFile()) return self.emit("error", new Error("Can't write on: " + self.name + " (it is not a file)")); - - if(self.options.initialRotation) { - var prev; - - self._interval(self.now()); - prev = self.prev; - self._interval(stats.mtime.getTime()); - - if(prev !== self.prev) return self.rotate(); - } - - self.size = stats.size; - - if(! self.options.size || stats.size < self.options.size) return self.open(); - - if(self.options.interval) self._interval(self.now()); - - self.rotate(); - }); -}; - -RotatingFileStream.prototype.immutate = function(first, index, now) { - if(! index) { - index = 1; - now = new Date(this.now()); - } - - if(index >= 1001) return this.emit("error", this.exhausted()); - - try { - this.name = this.generator(now, index); - } - catch(e) { - return this.emit("error", e); - } - - var open = function(size) { - this.size = size; - this.open(); - this.once( - "open", - function() { - if(! first) this.emit("rotated", this.last); - - this.last = this.name; - this.interval(); - }.bind(this) - ); - }.bind(this); - - fs.stat( - this.name, - function(err, stats) { - if(err) { - if(err.code === "ENOENT") return open(0); - - return this.emit("error", err); - } - - if(! stats.isFile()) return this.emit("error", new Error("Can't write on: " + this.name + " (it is not a file)")); - - if(this.options.size && stats.size >= this.options.size) return this.immutate(first, index + 1, now); - - open(stats.size); - }.bind(this) - ); -}; - -RotatingFileStream.prototype.move = function(retry) { - var name; - var self = this; - - var callback = function(err) { - if(err) return self.emit("error", err); - - self.open(); - - if(self.options.compress) self.compress(name); - else { - self.emit("rotated", name); - self.interval(); - } - }; - - this.findName({}, self.options.compress, function(err, found) { - if(err) return callback(err); - - name = found; - - fs.rename(self.name, name, function(err) { - if(err && err.code !== "ENOENT" && ! retry) return callback(err); - - if(! err) return callback(); - - utils.makePath(name, function(err) { - if(err) return callback(err); - - self.move(true); - }); - }); - }); -}; - -RotatingFileStream.prototype.now = function() { - return Date.now(); -}; - -RotatingFileStream.prototype.open = function(retry) { - var fd; - var self = this; - var options = { flags: "a" }; - var callback = function(err) { - if(err) self.emit("error", err); - - process.nextTick(self._rewrite.bind(self)); - }; - - if("mode" in this.options) options.mode = this.options.mode; - - var stream = fs.createWriteStream(this.name, options); - - stream.once("open", function() { - self.stream = stream; - self.emit("open", self.name); - - callback(); - }); - - stream.once("error", function(err) { - if(err.code !== "ENOENT" && ! retry) return callback(err); - - utils.makePath(self.name, function(err) { - if(err) return callback(err); - - self.open(true); - }); - }); -}; - -RotatingFileStream.prototype.rotate = function() { - this.size = 0; - this.rotation = new Date(); - - this.emit("rotation"); - this._clear(); - this._close(this.options.rotate ? this.classical.bind(this, this.options.rotate) : this.options.immutable ? this.immutate.bind(this) : this.move.bind(this)); -}; - -for(var i in compress) RotatingFileStream.prototype[i] = compress[i]; -for(i in interval) RotatingFileStream.prototype[i] = interval[i]; - -module.exports = RotatingFileStream; -module.exports.default = RotatingFileStream; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..273d1cb --- /dev/null +++ b/index.ts @@ -0,0 +1,879 @@ +"use strict"; + +import { ChildProcess, exec } from "child_process"; +import { Gzip, createGzip } from "zlib"; +import { Readable, Writable } from "stream"; +import { Stats, close, createReadStream, createWriteStream, mkdir, open, readFile, rename, stat, unlink, write, writeFile } from "fs"; +import { parse, sep } from "path"; +import { TextDecoder } from "util"; + +class RotatingFileStreamError extends Error { + public code: string; + + constructor() { + super("Too many destination file attempts"); + + this.code = "RFS-TOO-MANY"; + } +} + +export type Compressor = (source: string, dest: string) => string; +export type Generator = (time: number | Date, index?: number) => string; + +export interface Options { + compress?: boolean | string | Compressor; + encoding?: string; + history?: string; + immutable?: boolean; + initialRotation?: boolean; + interval?: string; + intervalBoundary?: boolean; + maxFiles?: number; + maxSize?: string; + mode?: number; + path?: string; + rotate?: number; + size?: string; +} + +interface Opts { + compress?: string | Compressor; + encoding?: string; + history?: string; + immutable?: boolean; + initialRotation?: boolean; + interval?: { num: number; unit: string }; + intervalBoundary?: boolean; + maxFiles?: number; + maxSize?: number; + mode?: number; + path?: string; + rotate?: number; + size?: number; +} + +type Callback = (error?: Error) => void; + +interface Chunk { + chunk: Buffer; + encoding: string; + next: Chunk; +} + +interface History { + name: string; + size: number; + time: number; +} + +export class RotatingFileStream extends Writable { + private createGzip: () => Gzip; + private destroyer: () => void; + private error: Error; + private exec: (command: string, callback?: (error: Error) => void) => ChildProcess; + private filename: string; + private finished: boolean; + private fsClose: (fd: number, callback: Callback) => void; + private fsCreateReadStream: (path: string, options: { flags?: string; mode?: number }) => Readable; + private fsCreateWriteStream: (path: string, options: { flags?: string; mode?: number }) => Writable; + private fsMkdir: (path: string, callback: Callback) => void; + private fsOpen: (path: string, flags: string, mode: number, callback: (error: NodeJS.ErrnoException, fd: number) => void) => void; + private fsReadFile: (path: string, encoding: string, callback: (error: NodeJS.ErrnoException, data: string) => void) => void; + private fsRename: (oldPath: string, newPath: string, callback: (error: NodeJS.ErrnoException) => void) => void; + private fsStat: (path: string, callback: (error: NodeJS.ErrnoException, stats: Stats) => void) => void; + private fsUnlink: (path: string, callback: Callback) => void; + private fsWrite: (fd: number, data: string, encoding: string, callback: Callback) => void; + private fsWriteFile: (path: string, data: string, encoding: string, callback: Callback) => void; + private generator: Generator; + private last: string; + private maxTimeout: number; + private next: number; + private opened: () => void; + private options: Opts; + private prev: number; + private rotatedName: string; + private rotation: Date; + private size: number; + private stream: Writable; + private timer: NodeJS.Timeout; + + constructor(generator: Generator, options: Opts) { + const { encoding, history, maxFiles, maxSize, path } = options; + + super({ decodeStrings: true, defaultEncoding: encoding }); + + this.createGzip = createGzip; + this.exec = exec; + this.filename = path + generator(null); + this.fsClose = close; + this.fsCreateReadStream = createReadStream; + this.fsCreateWriteStream = createWriteStream; + this.fsMkdir = mkdir; + this.fsOpen = open; + this.fsReadFile = readFile; + this.fsRename = rename; + this.fsStat = stat; + this.fsUnlink = unlink; + this.fsWrite = (write as unknown) as (fd: number, data: string, encoding: string, callback: Callback) => void; + this.fsWriteFile = writeFile; + this.generator = generator; + this.maxTimeout = 2147483640; + this.options = options; + + if(maxFiles || maxSize) options.history = path + (history ? history : this.generator(null) + ".txt"); + + this.on("close", () => (this.finished ? null : this.emit("finish"))); + this.on("finish", () => (this.finished = this.clear())); + + process.nextTick(() => + this.init(error => { + this.error = error; + if(this.opened) this.opened(); + }) + ); + } + + _destroy(error: Error, callback: Callback): void { + const destroyer = (): void => { + this.clear(); + this.reclose(() => {}); + }; + + if(this.stream) destroyer(); + else this.destroyer = destroyer; + + callback(error); + } + + _final(callback: Callback): void { + if(this.stream) return this.stream.end(callback); + callback(); + } + + _write(chunk: Buffer, encoding: string, callback: Callback): void { + this.rewrite({ chunk, encoding, next: null }, callback); + } + + _writev(chunks: Chunk[], callback: Callback): void { + this.rewrite(chunks[0], callback); + } + + private rewrite(chunk: Chunk, callback: Callback): void { + const destroy = (error: Error): void => { + this.destroy(); + + return callback(error); + }; + + const rewrite = (): void => { + if(this.destroyed) return callback(this.error); + if(this.error) return destroy(this.error); + + const done: Callback = (error?: Error): void => { + if(error) return destroy(error); + if(chunk.next) return this.rewrite(chunk.next, callback); + callback(); + }; + + this.size += chunk.chunk.length; + this.stream.write(chunk.chunk, chunk.encoding, (error: Error): void => { + if(error) return done(error); + if(this.options.size && this.size >= this.options.size) return this.rotate(done); + done(); + }); + }; + + if(this.stream) { + return this.fsStat(this.filename, (error: NodeJS.ErrnoException): void => { + if(! error) return rewrite(); + if(error.code !== "ENOENT") return destroy(error); + + this.reclose(() => this.reopen(false, 0, () => rewrite())); + }); + } + + this.opened = rewrite; + } + + private init(callback: Callback): void { + const { immutable, initialRotation, interval, size } = this.options; + + if(immutable) return this.immutate(true, callback); + + this.fsStat(this.filename, (error, stats) => { + if(error) return error.code === "ENOENT" ? this.reopen(false, 0, callback) : callback(error); + + if(! stats.isFile()) return callback(new Error(`Can't write on: ${this.filename} (it is not a file)`)); + + if(initialRotation) { + this.intervalBounds(this.now()); + const prev = this.prev; + this.intervalBounds(new Date(stats.mtime.getTime())); + + if(prev !== this.prev) return this.rotate(callback); + } + + this.size = stats.size; + + if(! size || stats.size < size) return this.reopen(false, stats.size, callback); + + if(interval) this.intervalBounds(this.now()); + + this.rotate(callback); + }); + } + + private makePath(name: string, callback: Callback): void { + const dir = parse(name).dir; + + this.fsMkdir(dir, (error: NodeJS.ErrnoException): void => { + if(error) { + if(error.code === "ENOENT") return this.makePath(dir, (error: Error): void => (error ? callback(error) : this.makePath(name, callback))); + return callback(error); + } + + callback(); + }); + } + + private reopen(retry: boolean, size: number, callback: Callback): void { + const options: any = { flags: "a" }; + + if("mode" in this.options) options.mode = this.options.mode; + + let called: boolean; + const stream = this.fsCreateWriteStream(this.filename, options); + + const end: Callback = (error?: Error): void => { + if(called) { + this.error = error; + return; + } + + called = true; + this.stream = stream; + + if(this.opened) { + process.nextTick(this.opened); + this.opened = null; + } + + if(this.destroyer) process.nextTick(this.destroyer); + + callback(error); + }; + + stream.once("open", () => { + this.size = size; + end(); + this.interval(); + this.emit("open", this.filename); + }); + + stream.once("error", (error: NodeJS.ErrnoException) => + error.code !== "ENOENT" || retry ? end(error) : this.makePath(this.filename, (error: Error): void => (error ? end(error) : this.reopen(true, size, callback))) + ); + } + + private reclose(callback: Callback): void { + const { stream } = this; + + if(! stream) return callback(); + + this.stream = null; + stream.once("finish", callback); + stream.end(); + } + + private now(): Date { + return new Date(); + } + + private rotate(callback: Callback): void { + const { immutable, rotate } = this.options; + + this.size = 0; + this.rotation = this.now(); + + this.clear(); + this.reclose(() => (rotate ? this.classical(rotate, callback) : immutable ? this.immutate(false, callback) : this.move(callback))); + this.emit("rotation"); + } + + private findName(tmp: boolean, callback: (error: Error, filename?: string) => void, index?: number): void { + if(! index) index = 1; + + const { interval, path, intervalBoundary } = this.options; + let filename = `${this.filename}.${index}.rfs.tmp`; + + if(index >= 1000) return callback(new RotatingFileStreamError()); + + if(! tmp) { + try { + filename = path + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index); + } catch(e) { + return callback(e); + } + } + + this.fsStat(filename, error => { + if(! error || error.code !== "ENOENT") return this.findName(tmp, callback, index + 1); + + callback(null, filename); + }); + } + + private move(callback: Callback): void { + const { compress } = this.options; + let filename: string; + + const open = (error?: Error): void => { + if(error) return callback(error); + + this.rotated(filename, callback); + }; + + this.findName(false, (error: Error, found: string): void => { + if(error) return callback(error); + + filename = found; + + this.touch(filename, false, (error: Error): void => { + if(error) return callback(error); + + if(compress) return this.compress(filename, open); + this.fsRename(this.filename, filename, open); + }); + }); + } + + private touch(filename: string, retry: boolean, callback: Callback): void { + this.fsOpen(filename, "a", parseInt("666", 8), (error: NodeJS.ErrnoException, fd: number) => { + if(error) { + if(error.code !== "ENOENT" || retry) return callback(error); + + return this.makePath(filename, error => { + if(error) return callback(error); + + this.touch(filename, true, callback); + }); + } + + return this.fsClose(fd, (error: Error): void => { + if(error) return callback(error); + + this.fsUnlink(filename, (error: Error): void => { + if(error) this.emit("warning", error); + callback(); + }); + }); + }); + } + + private classical(count: number, callback: Callback): void { + const { compress, path, rotate } = this.options; + let prevName: string; + let thisName: string; + + if(rotate === count) delete this.rotatedName; + + const open = (error?: Error): void => { + if(error) return callback(error); + + this.rotated(this.rotatedName, callback); + }; + + try { + prevName = count === 1 ? this.filename : path + this.generator(count - 1); + thisName = path + this.generator(count); + } catch(e) { + return callback(e); + } + + const next = count === 1 ? open : (): void => this.classical(count - 1, callback); + + const move = (): void => { + if(count === 1 && compress) return this.compress(thisName, open); + + this.fsRename(prevName, thisName, (error: NodeJS.ErrnoException): void => { + if(! error) return next(); + + if(error.code !== "ENOENT") return callback(error); + + this.makePath(thisName, (error: Error): void => { + if(error) return callback(error); + + this.fsRename(prevName, thisName, (error: Error): void => (error ? callback(error) : next())); + }); + }); + }; + + this.fsStat(prevName, (error: NodeJS.ErrnoException): void => { + if(error) { + if(error.code !== "ENOENT") return callback(error); + + return next(); + } + + if(! this.rotatedName) this.rotatedName = thisName; + + move(); + }); + } + + private clear(): boolean { + if(this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + + return true; + } + + private intervalBoundsBig(now: Date): void { + let year = now.getFullYear(); + let month = now.getMonth(); + let day = now.getDate(); + let hours = now.getHours(); + const { num, unit } = this.options.interval; + + if(unit === "M") { + day = 1; + hours = 0; + } else if(unit === "d") hours = 0; + else hours = parseInt(((hours / num) as unknown) as string, 10) * num; + + this.prev = new Date(year, month, day, hours, 0, 0, 0).getTime(); + + if(unit === "M") month += num; + else if(unit === "d") day += num; + else hours += num; + + this.next = new Date(year, month, day, hours, 0, 0, 0).getTime(); + } + + private intervalBounds(now: Date): Date { + const unit = this.options.interval.unit; + + if(unit === "M" || unit === "d" || unit === "h") this.intervalBoundsBig(now); + else { + let period = 1000 * this.options.interval.num; + + if(unit === "m") period *= 60; + + this.prev = parseInt(((now.getTime() / period) as unknown) as string, 10) * period; + this.next = this.prev + period; + } + + return new Date(this.prev); + } + + private interval(): void { + if(! this.options.interval) return; + + this.intervalBounds(this.now()); + + const set = (): void => { + const time = this.next - this.now().getTime(); + + this.timer = time > this.maxTimeout ? setTimeout(set, this.maxTimeout) : setTimeout(() => this.rotate(error => (this.error = error)), time); + this.timer.unref(); + }; + + set(); + } + + private compress(filename: string, callback: Callback): void { + const { compress } = this.options; + + const done = (error?: Error): void => { + if(error) return callback(error); + + this.fsUnlink(this.filename, callback); + }; + + if(typeof compress === "function") this.external(filename, done); + else this.gzip(filename, done); + } + + private external(filename: string, callback: Callback): void { + const compress: Compressor = this.options.compress as Compressor; + let cont: string; + + try { + cont = compress(this.filename, filename); + } catch(e) { + return callback(e); + } + + this.findName(true, (error: Error, found: string): void => { + if(error) return callback(error); + + this.fsOpen(found, "w", parseInt("777", 8), (error: Error, fd: number): void => { + if(error) return callback(error); + + const unlink = (error: Error): void => { + this.fsUnlink(found, (error2: Error): void => { + if(error2) this.emit("warning", error2); + + callback(error); + }); + }; + + this.fsWrite(fd, cont, "utf8", (error: Error): void => { + this.fsClose(fd, (error2: Error): void => { + if(error) { + if(error2) this.emit("warning", error2); + + return unlink(error); + } + + if(error2) return unlink(error2); + + if(found.indexOf(sep) === -1) found = `.${sep}${found}`; + + this.exec(found, unlink); + }); + }); + }); + }); + } + + private gzip(filename: string, callback: Callback): void { + const { mode } = this.options; + const options = mode ? { mode } : {}; + const inp = this.fsCreateReadStream(this.filename, {}); + const out = this.fsCreateWriteStream(filename, options); + const zip = this.createGzip(); + + [inp, out, zip].map(stream => stream.once("error", callback)); + out.once("finish", callback); + + inp.pipe(zip).pipe(out); + } + + private rotated(filename: string, callback: Callback): void { + const { maxFiles, maxSize } = this.options; + + const open = (error?: Error): void => { + if(error) return callback(error); + + this.reopen(false, 0, callback); + this.emit("rotated", filename); + }; + + if(maxFiles || maxSize) return this.history(filename, open); + open(); + } + + private history(filename: string, callback: Callback): void { + let { history } = this.options; + + this.fsReadFile(history, "utf8", (error: NodeJS.ErrnoException, data: string): void => { + if(error) { + if(error.code !== "ENOENT") return callback(error); + + return this.historyGather([filename], 0, [], callback); + } + + const files = data.split("\n"); + + files.push(filename); + this.historyGather(files, 0, [], callback); + }); + } + + private historyGather(files: string[], index: number, res: History[], callback: Callback): void { + if(index === files.length) return this.historyCheckFiles(res, callback); + + this.fsStat(files[index], (error: NodeJS.ErrnoException, stats: Stats): void => { + if(error) { + if(error.code !== "ENOENT") return callback(error); + } else if(stats.isFile()) { + res.push({ + name: files[index], + size: stats.size, + time: stats.ctime.getTime() + }); + } else this.emit("warning", new Error(`File '${files[index]}' contained in history is not a regular file`)); + + this.historyGather(files, index + 1, res, callback); + }); + } + + private historyRemove(files: History[], size: boolean, callback: Callback): void { + const file = files.shift(); + + this.fsUnlink(file.name, (error: NodeJS.ErrnoException): void => { + if(error) return callback(error); + + this.emit("removed", file.name, ! size); + callback(); + }); + } + + private historyCheckFiles(files: History[], callback: Callback): void { + const { maxFiles } = this.options; + + files.sort((a, b) => a.time - b.time); + + if(! maxFiles || files.length <= maxFiles) return this.historyCheckSize(files, callback); + + this.historyRemove(files, false, (error: Error): void => (error ? callback(error) : this.historyCheckFiles(files, callback))); + } + + private historyCheckSize(files: History[], callback: Callback): void { + const { maxSize } = this.options; + let size = 0; + + if(! maxSize) return this.historyWrite(files, callback); + + files.map(e => (size += e.size)); + + if(size <= maxSize) return this.historyWrite(files, callback); + + this.historyRemove(files, true, (error: Error): void => (error ? callback(error) : this.historyCheckSize(files, callback))); + } + + private historyWrite(files: History[], callback: Callback): void { + this.fsWriteFile(this.options.history, files.map(e => e.name).join("\n") + "\n", "utf8", (error: NodeJS.ErrnoException): void => { + if(error) return callback(error); + + this.emit("history"); + callback(); + }); + } + + private immutate(first: boolean, callback: Callback, index?: number, now?: Date): void { + if(! index) { + index = 1; + now = this.now(); + } + + if(index >= 1001) return callback(new RotatingFileStreamError()); + + try { + this.filename = this.options.path + this.generator(now, index); + } catch(e) { + return callback(e); + } + + const open = (size: number, callback: Callback): void => { + if(first) { + this.last = this.filename; + return this.reopen(false, size, callback); + } + + this.rotated(this.last, (error: Error): void => { + this.last = this.filename; + callback(error); + }); + }; + + this.fsStat(this.filename, (error: NodeJS.ErrnoException, stats: Stats): void => { + const { size } = this.options; + + if(error) { + if(error.code === "ENOENT") return open(0, callback); + + return callback(error); + } + + if(! stats.isFile()) return callback(new Error(`Can't write on: '${this.filename}' (it is not a file)`)); + + if(size && stats.size >= size) return this.immutate(first, callback, index + 1, now); + + open(stats.size, callback); + }); + } +} + +function buildNumberCheck(field: string): (type: string, options: Options, value: string) => void { + return (type: string, options: Options, value: string): void => { + const converted: number = parseInt(value, 10); + + if(type !== "number" || (converted as unknown) !== value || converted <= 0) throw new Error(`'${field}' option must be a positive integer number`); + }; +} + +function buildStringCheck(field: string, check: (value: string) => any) { + return (type: string, options: Options, value: string): void => { + if(type !== "string") throw new Error(`Don't know how to handle 'options.${field}' type: ${type}`); + + options[field] = check(value); + }; +} + +function checkMeasure(value: string, what: string, units: any): any { + const ret: any = {}; + + ret.num = parseInt(value, 10); + + if(isNaN(ret.num)) throw new Error(`Unknown 'options.${what}' format: ${value}`); + if(ret.num <= 0) throw new Error(`A positive integer number is expected for 'options.${what}'`); + + ret.unit = value.replace(/^[ 0]*/g, "").substr((ret.num + "").length, 1); + + if(ret.unit.length === 0) throw new Error(`Missing unit for 'options.${what}'`); + if(! units[ret.unit]) throw new Error(`Unknown 'options.${what}' unit: ${ret.unit}`); + + return ret; +} + +const intervalUnits: any = { + M: true, + d: true, + h: true, + m: true, + s: true +}; + +function checkIntervalUnit(ret: any, unit: string, amount: number): void { + if(parseInt(((amount / ret.num) as unknown) as string, 10) * ret.num !== amount) throw new Error(`An integer divider of ${amount} is expected as ${unit} for 'options.interval'`); +} + +function checkInterval(value: string): any { + const ret = checkMeasure(value, "interval", intervalUnits); + + switch(ret.unit) { + case "h": + checkIntervalUnit(ret, "hours", 24); + break; + + case "m": + checkIntervalUnit(ret, "minutes", 60); + break; + + case "s": + checkIntervalUnit(ret, "seconds", 60); + break; + } + + return ret; +} + +const sizeUnits: any = { + B: true, + G: true, + K: true, + M: true +}; + +function checkSize(value: string): any { + const ret = checkMeasure(value, "size", sizeUnits); + + if(ret.unit === "K") return ret.num * 1024; + if(ret.unit === "M") return ret.num * 1048576; + if(ret.unit === "G") return ret.num * 1073741824; + + return ret.num; +} + +const checks: any = { + compress: (type: string, options: Opts, value: boolean | string | Compressor): any => { + if(! value) throw new Error("A value for 'options.compress' must be specified"); + if(type === "boolean") return (options.compress = (source: string, dest: string): string => `cat ${source} | gzip -c9 > ${dest}`); + if(type === "function") return; + if(type !== "string") throw new Error(`Don't know how to handle 'options.compress' type: ${type}`); + if(((value as unknown) as string) !== "gzip") throw new Error(`Don't know how to handle compression method: ${value}`); + }, + + encoding: (type: string, options: Opts, value: string): any => new TextDecoder(value), + + history: (type: string): void => { + if(type !== "string") throw new Error(`Don't know how to handle 'options.history' type: ${type}`); + }, + + immutable: (): void => {}, + + initialRotation: (): void => {}, + + interval: buildStringCheck("interval", checkInterval), + + intervalBoundary: (): void => {}, + + maxFiles: buildNumberCheck("maxFiles"), + + maxSize: buildStringCheck("maxSize", checkSize), + + mode: (): void => {}, + + path: (type: string, options: Opts, value: string): void => { + if(type !== "string") throw new Error(`Don't know how to handle 'options.path' type: ${type}`); + if(value[value.length - 1] !== sep) options.path = value + sep; + }, + + rotate: buildNumberCheck("rotate"), + + size: buildStringCheck("size", checkSize) +}; + +function checkOpts(options: Options): Opts { + const ret: Opts = {}; + + for(const opt in options) { + const value = options[opt]; + const type = typeof value; + + if(! (opt in checks)) throw new Error(`Unknown option: ${opt}`); + + ret[opt] = options[opt]; + checks[opt](type, ret, value); + } + + if(! ret.path) ret.path = ""; + + if(! ret.interval) { + delete ret.immutable; + delete ret.initialRotation; + delete ret.intervalBoundary; + } + + if(ret.rotate) { + delete ret.history; + delete ret.immutable; + delete ret.maxFiles; + delete ret.maxSize; + delete ret.intervalBoundary; + } + + if(ret.immutable) delete ret.compress; + + if(! ret.intervalBoundary) delete ret.initialRotation; + + return ret; +} + +function createClassical(filename: string): Generator { + return (index: number): string => (index ? `${filename}.${index}` : filename); +} + +function createGenerator(filename: string): Generator { + const pad = (num: number): string => (num > 9 ? "" : "0") + num; + + return (time: Date, index?: number): string => { + if(! time) return (filename as unknown) as string; + + const month = time.getFullYear() + "" + pad(time.getMonth() + 1); + const day = pad(time.getDate()); + const hour = pad(time.getHours()); + const minute = pad(time.getMinutes()); + + return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename; + }; +} + +export function createStream(filename: string | Generator, options?: Options): RotatingFileStream { + if(typeof options === "undefined") options = {}; + else if(typeof options !== "object") throw new Error(`The "options" argument must be of type object. Received type ${typeof options}`); + + const opts = checkOpts(options); + + let generator: Generator; + + if(typeof filename === "string") generator = options.rotate ? createClassical(filename) : createGenerator(filename); + else if(typeof filename === "function") generator = filename; + else throw new Error(`The "filename" argument must be one of type string or function. Received type ${typeof filename}`); + + return new RotatingFileStream(generator, opts); +} diff --git a/interval.js b/interval.js deleted file mode 100644 index a76d6ef..0000000 --- a/interval.js +++ /dev/null @@ -1,168 +0,0 @@ -"use strict"; - -var fs = require("fs"); -var util = require("util"); - -function _clear(done) { - if(this.timer) { - clearTimeout(this.timer); - this.timer = null; - } -} - -function __interval(now) { - now = new Date(now); - var year = now.getFullYear(); - var month = now.getMonth(); - var day = now.getDate(); - var hours = now.getHours(); - var num = this.options.interval.num; - var unit = this.options.interval.unit; - - if(unit === "M") { - day = 1; - hours = 0; - } - else if(unit === "d") hours = 0; - else hours = parseInt(hours / num, 10) * num; - - this.prev = new Date(year, month, day, hours, 0, 0, 0).getTime(); - - if(unit === "M") month += num; - else if(unit === "d") day += num; - else hours += num; - - this.next = new Date(year, month, day, hours, 0, 0, 0).getTime(); -} - -function _interval(now) { - var unit = this.options.interval.unit; - - if(unit === "M" || unit === "d" || unit === "h") this.__interval(now); - else { - var period = 1000 * this.options.interval.num; - - if(unit === "m") period *= 60; - - this.prev = parseInt(now / period, 10) * period; - this.next = this.prev + period; - } - - return new Date(this.prev); -} - -function interval() { - if(! this.options.interval) return; - - this._interval(this.now()); - - var self = this; - var set = function() { - var time = self.next - self.now(); - - self.timer = time > self.maxTimeout ? setTimeout(set, self.maxTimeout) : setTimeout(self.rotate.bind(self), time); - self.timer.unref(); - }; - - set(); -} - -function historyWrite(self, res) { - var files = []; - - res.map(e => files.push(e.name)); - self.files = files; - - fs.writeFile(self.options.history, files.join("\n"), "utf8", function(err) { - if(err) self.emit("warning", err); - - self.emit("history"); - }); -} - -function historyRemove(self, res, step, number) { - var file = res.shift(); - - fs.unlink(file.name, function(err) { - if(err) self.emit("warning", err); - else self.emit("removed", file.name, number); - - step(self, res); - }); -} - -function historyCheckSize(self, res) { - if(! self.options.maxSize) return historyWrite(self, res); - - var size = 0; - - res.map(e => (size += e.size)); - - if(size <= self.options.maxSize) return historyWrite(self, res); - - historyRemove(self, res, historyCheckSize, false); -} - -function historyCheckFiles(self, res) { - res.sort(function(a, b) { - return a.time - b.time; - }); - - if(! self.options.maxFiles || res.length <= self.options.maxFiles) return historyCheckSize(self, res); - - historyRemove(self, res, historyCheckFiles, true); -} - -function historyGather(self, files, idx, res) { - if(idx === files.length) return historyCheckFiles(self, res); - - fs.stat(files[idx], function(err, stats) { - if(err) { - if(err.code !== "ENOENT") return self.emit("warning", err); - } - else if(stats.isFile()) - res.push({ - name: files[idx], - size: stats.size, - time: stats.ctime.getTime() - }); - else self.emit("warning", "File '" + files[idx] + "' contained in history is not a regular file"); - - historyGather(self, files, idx + 1, res); - }); -} - -function history(lastfile) { - var filename = this.options.history; - var self = this; - - if(this.files) { - this.files.push(lastfile); - - return historyGather(self, this.files, 0, []); - } - - if(! filename) this.options.history = filename = this.generator(null) + ".txt"; - - fs.readFile(filename, "utf8", function(err, data) { - if(err) { - if(err.code !== "ENOENT") return self.emit("warning", err); - - return historyGather(self, [lastfile], 0, []); - } - - var files = data.split("\n"); - - files.push(lastfile); - historyGather(self, files, 0, []); - }); -} - -module.exports = { - __interval: __interval, - _clear: _clear, - _interval: _interval, - history: history, - interval: interval, - maxTimeout: 2147483640 -}; diff --git a/package-lock.json b/package-lock.json index cc5e79e..a71a4d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,12 +111,103 @@ "to-fast-properties": "^2.0.0" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, "@types/node": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", - "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", + "version": "12.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.12.tgz", + "integrity": "sha512-MGuvYJrPU0HUwqF7LqvIj50RZUX23Z+m583KBygKYUZLlZ88n6w28XRNJRJgsHukLEnLz6w6SvxZoLgbr5wLqQ==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz", + "integrity": "sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.8.0", + "eslint-utils": "^1.4.3", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz", + "integrity": "sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.8.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.8.0.tgz", + "integrity": "sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.8.0", + "@typescript-eslint/typescript-estree": "2.8.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz", + "integrity": "sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash.unescape": "4.0.1", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "acorn": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", @@ -124,9 +215,9 @@ "dev": true }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, "ajv": { @@ -148,10 +239,13 @@ "dev": true }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } }, "ansi-regex": { "version": "3.0.0", @@ -183,6 +277,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "arg": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", + "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -220,6 +320,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "caching-transform": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", @@ -262,12 +368,12 @@ "dev": true }, "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" } }, "cli-width": { @@ -309,9 +415,9 @@ "dev": true }, "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -482,9 +588,9 @@ "dev": true }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.0.tgz", + "integrity": "sha512-dQpj+PaHKHfXHQ2Imcw5d853PTvkUGbHk/MR68KQUl98EgKDCdh4vLRH1ZxhqeQjQFJeg8fgN0UwmNhN3l8dDQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -494,19 +600,19 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -515,7 +621,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -532,6 +638,21 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -560,12 +681,12 @@ } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { @@ -575,13 +696,13 @@ "dev": true }, "espree": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", - "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", "dev": true, "requires": { - "acorn": "^7.0.0", - "acorn-jsx": "^5.0.2", + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -666,9 +787,9 @@ "dev": true }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -826,9 +947,9 @@ "dev": true }, "handlebars": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz", - "integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -903,9 +1024,9 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -935,32 +1056,66 @@ "dev": true }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", "dev": true, "requires": { - "ansi-escapes": "^3.2.0", + "ansi-escapes": "^4.2.1", "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", + "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", "run-async": "^2.2.0", "rxjs": "^6.4.0", - "string-width": "^2.1.0", + "string-width": "^4.1.0", "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -968,6 +1123,14 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } } } @@ -1250,6 +1413,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -1279,6 +1448,12 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -1325,9 +1500,9 @@ } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "minimatch": { @@ -1494,9 +1669,9 @@ "dev": true }, "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, "natural-compare": { @@ -1637,12 +1812,12 @@ } }, "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" } }, "optimist": { @@ -1653,28 +1828,20 @@ "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "word-wrap": "~1.2.3" } }, "os-homedir": { @@ -1835,6 +2002,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -1885,9 +2058,9 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", "dev": true }, "release-zalgo": { @@ -1927,12 +2100,12 @@ "dev": true }, "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, @@ -2025,6 +2198,24 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "spawn-wrap": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", @@ -2228,12 +2419,42 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-node": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.2.tgz", + "integrity": "sha512-W1DK/a6BGoV/D4x/SXXm6TSQx6q3blECUzd5TN+j56YEMX3yPVMpHsICLedUw3DvGF3aTQ8hfdR9AKMaHjIi+A==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -2243,20 +2464,26 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { @@ -2324,10 +2551,16 @@ "string-width": "^1.0.2 || 2" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", "dev": true }, "wrap-ansi": { @@ -2560,6 +2793,12 @@ } } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index 158a0ab..f561458 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,14 @@ "version": "1.4.6", "description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.", "scripts": { - "all": "npm run npmignore && npm run eslint && npm run coverage && npm run ts", - "coverage": "TZ=\"Europe/Rome\" ./node_modules/.bin/nyc -r lcov -r text -r text-summary npm test", - "debug": "node --inspect-brk ./node_modules/.bin/_mocha test", - "eslint": "./node_modules/.bin/eslint *.js test/*js", - "npmignore": "echo '.codeclimate.yml\\n.eslintrc\\n.gitignore\\n.gitattributes\\n.travis.yml\\n.vscode\\nCHANGELOG.md\\nREADME.md\\ntest' > .npmignore ; cat .gitignore >> .npmignore", - "test": "TZ=\"Europe/Rome\" ./node_modules/.bin/_mocha test", - "ts": "node_modules/.bin/tsc index.d.ts --lib es6" + "all": "npm run eslint && npm run coverage", + "clean": "node -r ts-node/register utils.ts clean", + "coverage": "tsc && TZ=\"Europe/Rome\" nyc -r lcov -r text -r text-summary -r html mocha -r ts-node/register test/*ts", + "eslint": "eslint index.ts utils.ts test/*ts", + "ignore": "node -r ts-node/register utils.ts ignore", + "prepare": "npm run ignore && tsc && npm run readme", + "readme": "node -r ts-node/register utils.ts readme", + "test": "npm run clean && mocha -r ts-node/register test/*ts" }, "bugs": "https://github.com/iccicci/rotating-file-stream/issues", "repository": "https://github.com/iccicci/rotating-file-stream", @@ -19,7 +20,7 @@ "logrotate" ], "engines": { - "node": ">=6.0" + "node": ">=10.0" }, "author": "Daniele Ricci (https://github.com/iccicci)", "contributors": [ @@ -32,13 +33,21 @@ "cchare (https://github.com/cchare)" ], "license": "MIT", + "funding": { + "url": "https://www.blockchain.com/btc/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN" + }, "readmeFilename": "README.md", "types": "index.d.ts", "devDependencies": { - "eslint": "6.5.1", + "@types/mocha": "5.2.7", + "@types/node": "12.12.12", + "@typescript-eslint/eslint-plugin": "2.8.0", + "@typescript-eslint/parser": "2.8.0", + "eslint": "6.7.0", "mocha": "6.2.2", "nyc": "14.1.1", - "typescript": "3.6.4", - "@types/node": "12.11.1" + "prettier": "1.19.1", + "ts-node": "8.5.2", + "typescript": "3.7.2" } } diff --git a/test/01rfs.js b/test/01rfs.js deleted file mode 100644 index 75fc778..0000000 --- a/test/01rfs.js +++ /dev/null @@ -1,399 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var fs = require("fs"); -var rfs = require(".."); -var Writable = require("stream").Writable; - -describe("rfs", function() { - describe("new", function() { - before(function(done) { - this.rfs = rfs("test.log", { highWaterMark: 1000, mode: parseInt("666", 8) }); - done(); - }); - - it("constructor", function() { - assert.equal(this.rfs instanceof rfs, true); - }); - - it("Writable", function() { - assert.equal(this.rfs instanceof Writable, true); - }); - - it("std filename generator first time", function() { - assert.equal(this.rfs.generator(null), "test.log"); - }); - - it("std filename generator later times", function() { - assert.equal(this.rfs.generator(new Date("1976-01-23 14:45"), 4), "19760123-1445-04-test.log"); - }); - }); - - describe("wrong filename type", function() { - before(function(done) { - try { - this.rfs = rfs({}); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'filename' type: object"); - }); - }); - - describe("no options", function() { - before(function(done) { - try { - this.rfs = rfs("test.log"); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("no error", function() { - assert.equal(this.err, null); - }); - }); - - describe("wrong options type", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", "test.log"); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options' type: string"); - }); - }); - - describe("unknown option", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { test: true }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Unknown option: test"); - }); - }); - - describe("no compress value", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { compress: false }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "A value for 'options.compress' must be specified"); - }); - }); - - describe("wrong compress type", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { compress: 23 }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options.compress' type: number"); - }); - }); - - describe("wrong compression method", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { compress: "test" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle compression method: test"); - }); - }); - - describe("wrong interval type", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { interval: 23 }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options.interval' type: number"); - }); - }); - - describe("wrong path type", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { path: 23 }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options.path' type: number"); - }); - }); - - describe("wrong size type", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { size: 23 }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options.size' type: number"); - }); - }); - - describe("wrong size format", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { size: "test" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Unknown 'options.size' format: test"); - }); - }); - - describe("wrong size number", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { size: "-23" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "A positive integer number is expected for 'options.size'"); - }); - }); - - describe("missing size unit", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { size: "23" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Missing unit for 'options.size'"); - }); - }); - - describe("wrong size unit", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { size: "23test" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Unknown 'options.size' unit: t"); - }); - }); - - describe("wrong interval secons number", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { interval: "23s" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "An integer divider of 60 is expected as seconds for 'options.interval'"); - }); - }); - - describe("wrong interval minutes number", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { interval: "23m" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "An integer divider of 60 is expected as minutes for 'options.interval'"); - }); - }); - - describe("wrong interval hours number", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { interval: "23h" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "An integer divider of 24 is expected as hours for 'options.interval'"); - }); - }); - - describe("string rotate value", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { rotate: "test" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "'rotate' option must be a positive integer number"); - }); - }); - - describe("negative rotate value", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { rotate: "-2" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "'rotate' option must be a positive integer number"); - }); - }); - - describe("wrong history", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { history: {} }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "Don't know how to handle 'options.history' type: object"); - }); - }); - - describe("wrong maxFiles", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { maxFiles: {} }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "'maxFiles' option must be a positive integer number"); - }); - }); - - describe("bad maxFiles", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { maxFiles: -2 }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "'maxFiles' option must be a positive integer number"); - }); - }); - - describe("wrong maxSize", function() { - before(function(done) { - try { - this.rfs = rfs("test.log", { maxSize: "-2" }); - } - catch(e) { - this.err = e; - } - done(); - }); - - it("error", function() { - assert.equal(this.err.message, "A positive integer number is expected for 'options.size'"); - }); - }); -}); diff --git a/test/01rfs.ts b/test/01rfs.ts new file mode 100644 index 0000000..f38b31f --- /dev/null +++ b/test/01rfs.ts @@ -0,0 +1,59 @@ +"use strict"; + +process.env.TZ = "Europe/Rome"; + +import { RotatingFileStream, createStream } from ".."; +import { strictEqual as eq, throws as ex } from "assert"; +import { Writable } from "stream"; + +describe("rfs", () => { + describe("new", () => { + let rfs: any; + + before(done => { + rfs = createStream("test.log", { mode: parseInt("666", 8) }); + rfs.end(done); + }); + + it("RFS", () => eq(rfs instanceof RotatingFileStream, true)); + it("Writable", () => eq(rfs instanceof Writable, true)); + it("std filename generator first time", () => eq(rfs.generator(null), "test.log")); + it("std filename generator later times", () => eq(rfs.generator(new Date("1976-01-23 14:45"), 4), "19760123-1445-04-test.log")); + }); + + describe("no options", () => { + before(done => createStream("test.log").end(done)); + + it("no error", () => eq(true, true)); + }); + + describe("wrong calls", () => { + const encodingError = RangeError("The \"test\" encoding is not supported"); + + if(Number(process.version.match(/^v(\d+)/)[1]) < 11) encodingError.name = "RangeError [ERR_ENCODING_NOT_SUPPORTED]"; + + it("wrong filename type", () => ex(() => createStream({} as string), Error("The \"filename\" argument must be one of type string or function. Received type object"))); + it("wrong options type", () => ex(() => createStream("test.log", "test.log" as unknown), Error("The \"options\" argument must be of type object. Received type string"))); + it("unknown option", () => ex(() => createStream("test.log", { test: true } as any), Error("Unknown option: test"))); + it("no compress value", () => ex(() => createStream("test.log", { compress: false }), Error("A value for 'options.compress' must be specified"))); + it("wrong compress type", () => ex(() => createStream("test.log", { compress: 23 } as any), Error("Don't know how to handle 'options.compress' type: number"))); + it("wrong compress method", () => ex(() => createStream("test.log", { compress: "test" }), Error("Don't know how to handle compression method: test"))); + it("wrong interval type", () => ex(() => createStream("test.log", { interval: 23 } as any), Error("Don't know how to handle 'options.interval' type: number"))); + it("wrong path type", () => ex(() => createStream("test.log", { path: 23 } as any), Error("Don't know how to handle 'options.path' type: number"))); + it("wrong size type", () => ex(() => createStream("test.log", { size: 23 } as any), Error("Don't know how to handle 'options.size' type: number"))); + it("wrong size type", () => ex(() => createStream("test.log", { size: "test" }), Error("Unknown 'options.size' format: test"))); + it("wrong size number", () => ex(() => createStream("test.log", { size: "-23B" }), Error("A positive integer number is expected for 'options.size'"))); + it("missing size unit", () => ex(() => createStream("test.log", { size: "23" }), Error("Missing unit for 'options.size'"))); + it("wrong size unit", () => ex(() => createStream("test.log", { size: "23test" }), Error("Unknown 'options.size' unit: t"))); + it("wrong interval seconds number", () => ex(() => createStream("test.log", { interval: "23s" }), Error("An integer divider of 60 is expected as seconds for 'options.interval'"))); + it("wrong interval minutes number", () => ex(() => createStream("test.log", { interval: "23m" }), Error("An integer divider of 60 is expected as minutes for 'options.interval'"))); + it("wrong interval hours number", () => ex(() => createStream("test.log", { interval: "23h" }), Error("An integer divider of 24 is expected as hours for 'options.interval'"))); + it("string rotate value", () => ex(() => createStream("test.log", { rotate: "test" } as any), Error("'rotate' option must be a positive integer number"))); + it("negative rotate value", () => ex(() => createStream("test.log", { rotate: -23 }), Error("'rotate' option must be a positive integer number"))); + it("wrong history", () => ex(() => createStream("test.log", { history: {} } as any), Error("Don't know how to handle 'options.history' type: object"))); + it("wrong maxFiles", () => ex(() => createStream("test.log", { maxFiles: {} } as any), Error("'maxFiles' option must be a positive integer number"))); + it("negative maxFiles", () => ex(() => createStream("test.log", { maxFiles: -23 }), Error("'maxFiles' option must be a positive integer number"))); + it("wrong maxSize", () => ex(() => createStream("test.log", { maxSize: "-23B" }), Error("A positive integer number is expected for 'options.size'"))); + it("wrong encoding", () => ex(() => createStream("test.log", { encoding: "test" }), encodingError)); + }); +}); diff --git a/test/02write.js b/test/02write.js deleted file mode 100644 index e851956..0000000 --- a/test/02write.js +++ /dev/null @@ -1,152 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("write(s)", function() { - describe("single write", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("multi write", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\ntest\ntest\ntest\n"); - }); - }); - - describe("end callback", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done); - self.rfs.end("test\n", function() { - self.endcb = true; - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("end callback", function() { - assert.equal(this.endcb, true); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("end too many parameters", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done); - self.rfs.end("test\n", "utf8", "dummy"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); -}); diff --git a/test/02write.ts b/test/02write.ts new file mode 100644 index 0000000..d27e4e3 --- /dev/null +++ b/test/02write.ts @@ -0,0 +1,108 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync, unlink } from "fs"; +import { test } from "./helper"; + +describe("write(s)", () => { + describe("single write", () => { + const events = test({}, rfs => rfs.end("test\n")); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + }); + + describe("multi write", () => { + const events = test({ files: { "test.log": "test\n" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\ntest\ntest\ntest\n")); + }); + + describe("end callback", function() { + const events = test({}, rfs => { + rfs.end("test\n", "utf8", () => (events.endcb = true)); + }); + + it("events", () => deq(events, { endcb: true, finish: 1, open: ["test.log"], write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + }); + + describe("write after open", function() { + const events = test({}, rfs => rfs.once("open", () => rfs.end("test\n", "utf8"))); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + }); + + describe("destroy before open", function() { + let stream: any; + let open: boolean; + + const event = (done?: any): void => { + open = true; + if(done) done(); + }; + + const events = test({}, rfs => { + stream = rfs; + rfs.on("open", () => event()); + rfs.destroy(); + rfs.write("test\n"); + }); + + before(done => { + if(open) return done(); + stream.on("open", () => event(done)); + }); + + it("events", () => deq(events, { close: 1, finish: 1, error: ["ERR_STREAM_DESTROYED"], open: ["test.log"] })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + }); + + describe("destroy between open and write", function() { + const events = test({}, rfs => + rfs.once("open", () => { + rfs.destroy(); + rfs.write("test\n"); + }) + ); + + it("events", () => deq(events, { close: 1, finish: 1, error: ["ERR_STREAM_DESTROYED"], open: ["test.log"] })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + }); + + describe("destroy while writing", function() { + const events = test({}, rfs => + rfs.once("open", () => { + rfs.write("test\n"); + rfs.destroy(); + }) + ); + + it("events", () => deq(events, { close: 1, finish: 1, open: ["test.log"], write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + }); + + describe("destroy after write", function() { + const events = test({}, rfs => + rfs.once("open", () => { + rfs.write("test\n", () => rfs.destroy()); + }) + ); + + it("events", () => deq(events, { close: 1, finish: 1, open: ["test.log"], write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + }); + + describe("remove file between writes", function() { + const events = test({}, rfs => rfs.write("test\n", () => unlink("test.log", () => rfs.end("test\n")))); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], write: 2 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + }); +}); diff --git a/test/03size.js b/test/03size.js deleted file mode 100644 index 256ae6f..0000000 --- a/test/03size.js +++ /dev/null @@ -1,168 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("size", function() { - describe("initial rotation", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B" }); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("1-test.log"), "test\ntest\n"); - }); - }); - - describe("single write rotation by size", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { size: "10B" }); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), ""); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("1-test.log"), "test\ntest\n"); - }); - }); - - describe("multi write rotation by size", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B" }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("1-test.log"), "test\ntest\n"); - }); - }); - - describe("one write one file", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { size: "15B" }); - self.rfs.write("test\n"); - self.rfs.write("test\ntest\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("1-test.log"), "test\ntest\ntest\ntest\n"); - }); - }); -}); diff --git a/test/03size.ts b/test/03size.ts new file mode 100644 index 0000000..54a3894 --- /dev/null +++ b/test/03size.ts @@ -0,0 +1,62 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync } from "fs"; +import { sep } from "path"; +import { test } from "./helper"; + +describe("size", () => { + describe("initial rotation", () => { + const events = test({ files: { "test.log": "test\ntest\n" }, options: { size: "10B" } }, rfs => rfs.end("test\n")); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], rotated: ["1-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("single write rotation by size", () => { + const events = test({ files: { "test.log": "test\n" }, options: { size: "10B" } }, rfs => rfs.end("test\n")); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("multi write rotation by size", () => { + const events = test({ options: { size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("one write one file", () => { + const events = test({ files: { "test.log": "test\n" }, options: { size: "15B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\ntest\ntest\n")); + }); + + describe("missing path creation", function() { + const filename = `log${sep}t${sep}test.log`; + const rotated = `log${sep}t${sep}t${sep}test.log`; + const events = test({ filename: (time: Date): string => (time ? rotated : filename), options: { size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: [filename, filename], rotated: [rotated], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync(filename, "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync(rotated, "utf8"), "test\ntest\n")); + }); +}); diff --git a/test/04errors.js b/test/04errors.js deleted file mode 100644 index f50aa84..0000000 --- a/test/04errors.js +++ /dev/null @@ -1,718 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("errors", function() { - describe("wrong name generator (first time)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B" }, function() { - throw new Error("test"); - }); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - }); - - describe("wrong name generator (rotation)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "15B" }, function(time) { - if(time) throw new Error("test"); - return "test.log"; - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\ntest\ntest\n"); - }); - }); - - describe("wrong name generator (immutable)", function() { - before(function(done) { - var self = this; - var first = true; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "5B" }, function(time) { - if(! first) throw new Error("test"); - first = false; - return "test.log"; - }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("logging on directory", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "5B" }, "test"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Can't write on: test (it is not a file)"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("logging on directory (immutable)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "5B" }, function() { - return "test"; - }); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Can't write on: test (it is not a file)"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("using file as directory", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "5B" }, "index.js/test.log"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "ENOTDIR"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("no rotated file available", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "5B" }, function(time, index) { - return "test.log"; - }); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Too many destination file attempts"); - assert.equal(this.rfs.err.attempts["test.log"], 1000); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("no rotated file available (initial rotation)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { size: "5B" }, function(time, index) { - return "test.log"; - }); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Too many destination file attempts"); - assert.equal(this.rfs.err.attempts["test.log"], 1000); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("error while write", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { interval: "10d" }); - self.rfs.once("open", function() { - self.rfs.stream.write = function(buffer, callback) { - process.nextTick(callback.bind(null, new Error("Test error"))); - }; - self.rfs.end("test\n"); - }); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Test error"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), ""); - }); - }); - - describe("error while rename", function() { - before(function(done) { - var self = this; - var oldR = fs.rename; - fs.rename = function(a, b, callback) { - process.nextTick(callback.bind(null, new Error("Test error"))); - }; - exec(done, "rm -rf *log", function() { - self.rfs = rfs( - function() { - fs.rename = oldR; - done(); - }, - { size: "5B" } - ); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "Test error"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - }); - - describe("missing path creation", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B" }, function(time) { - if(time) return "log/t/rot/test.log"; - return "log/t/test.log"; - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "log/t/rot/test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("log/t/test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("log/t/rot/test.log"), "test\ntest\n"); - }); - }); - - describe("error creating missing path in first open", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - var mkdir = fs.mkdir; - fs.mkdir = function(path, callback) { - process.nextTick(callback.bind(null, { code: "EACCES" })); - }; - self.rfs = rfs( - function() { - fs.mkdir = mkdir; - done(); - }, - {}, - function() { - return "log/t/test.log"; - } - ); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "EACCES"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error creating missing path in rotation", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - var mkdir = fs.mkdir; - fs.mkdir = function(path, callback) { - process.nextTick(callback.bind(null, { code: "EACCES" })); - }; - self.rfs = rfs( - function() { - fs.mkdir = mkdir; - done(); - }, - { size: "5B" }, - function(time) { - if(time) return "log/t/test.log"; - return "test.log"; - } - ); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "EACCES"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error on no rotated file open", function() { - before(function(done) { - var self = this; - var oldC = fs.createWriteStream; - fs.createWriteStream = function() { - return { - once: function(event, callback) { - if(event === "error") setTimeout(callback.bind(null, { code: "TEST" }), 50); - } - }; - }; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(function() { - fs.createWriteStream = oldC; - done(); - }); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error unlinking file", function() { - before(function(done) { - var self = this; - var oldU = fs.unlink; - exec(done, "rm -rf *log", function() { - fs.unlink = function(path, callback) { - setTimeout(function() { - fs.unlink = oldU; - setTimeout(done, 50); - callback({ code: "TEST" }); - }, 50); - }; - self.rfs = rfs(function() {}, { size: "5B", compress: "gzip" }); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error unlinking external compression file", function() { - before(function(done) { - var self = this; - var oldU = fs.unlink; - exec(done, "rm -rf *log", function() { - fs.unlink = function(path, callback) { - setTimeout(function() { - fs.unlink = oldU; - callback({ code: "TEST" }); - }, 50); - }; - self.rfs = rfs(done, { size: "5B", compress: true }); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error in external compression function", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { - size: "5B", - compress: function() { - var e = new Error("test"); - e.code = "TEST"; - throw e; - } - }); - self.rfs.write("test\n"); - }); - }); - - it("error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("no warning", function() { - assert.ifError(this.rfs.ev.warn); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error in stat (immutable)", function() { - before(function(done) { - var self = this; - var preS = fs.stat; - fs.stat = function(a, b) { - process.nextTick(b.bind(null, new Error("test"))); - }; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "5B" }); - self.rfs.on("error", function() { - fs.stat = preS; - }); - }); - }); - - it("error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("no warning", function() { - assert.ifError(this.rfs.ev.warn); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("immutable exhausted", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "5B" }, function() { - return "test.log"; - }); - }); - }); - - it("error", function() { - assert.equal(this.rfs.err.message, "Too many destination file attempts"); - }); - - it("no warning", function() { - assert.ifError(this.rfs.ev.warn); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("0 single write", function() { - assert.equal(this.rfs.ev.single, 0); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); -}); diff --git a/test/04errors.ts b/test/04errors.ts new file mode 100644 index 0000000..2dc4aef --- /dev/null +++ b/test/04errors.ts @@ -0,0 +1,257 @@ +"use strict"; + +import { close, mkdir, readFileSync } from "fs"; +import { deepStrictEqual as deq, strictEqual as eq, throws as ex } from "assert"; +import { createStream } from ".."; +import { sep } from "path"; +import { test } from "./helper"; + +describe("errors", () => { + describe("wrong name generator (first time)", () => { + it("wrong filename type", () => + ex( + () => + createStream(() => { + throw new Error("test"); + }), + Error("test") + )); + }); + + describe("wrong name generator (rotation)", () => { + const events = test( + { + filename: (time: Date) => { + if(time) throw new Error("test"); + return "test.log"; + }, + options: { size: "15B" } + }, + rfs => { + [0, 0, 0, 0].map(() => rfs.write("test\n")); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\ntest\ntest\n")); + }); + + describe("wrong name generator (immutable)", () => { + const events = test( + { + filename: (time: Date) => { + if(time) throw new Error("test"); + return "test.log"; + }, + options: { immutable: true, interval: "1d", size: "5B" } + }, + rfs => { + rfs.write("test\n"); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, write: 1 })); + }); + + describe("logging on directory", () => { + const events = test({ filename: "test", options: { size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["Can't write on: test (it is not a file)"], finish: 1, write: 1 })); + }); + + describe("logging on directory (immutable)", () => { + const events = test({ filename: () => "test", options: { immutable: true, interval: "1d", size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["Can't write on: 'test' (it is not a file)"], finish: 1, write: 1 })); + }); + + describe("using file as directory", () => { + const events = test({ filename: `index.ts${sep}test.log`, options: { size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["ENOTDIR"], finish: 1, write: 1 })); + }); + + describe("no rotated file available", () => { + const events = test({ filename: () => "test.log", options: { size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["RFS-TOO-MANY"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("no rotated file available", () => { + const events = test({ filename: () => "test.log", files: { "test.log": "test\n" }, options: { size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["RFS-TOO-MANY"], finish: 1, rotation: 1, write: 1 })); + }); + + describe("error while write", () => { + const events = test({ options: { interval: "10d" } }, rfs => + rfs.once("open", () => { + rfs.stream.write = (buffer: string, encoding: string, callback: any): void => { + process.nextTick(() => callback(new Error(encoding + buffer))); + }; + rfs.write("test\n"); + }) + ); + + it("events", () => deq(events, { close: 1, error: ["buffertest\n"], finish: 1, open: ["test.log"], write: 1 })); + }); + + describe("error while rename", () => { + const events = test({ options: { size: "5B" } }, rfs => { + rfs.fsRename = (a: string, b: string, callback: any): void => process.nextTick(() => callback(new Error(a + b))); + rfs.once("open", () => rfs.write("test\n")); + }); + + it("events", () => deq(events, { close: 1, error: ["test.log1-test.log"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error creating missing path (first open)", () => { + const filename = `log${sep}t${sep}test.log`; + const rotated = `log${sep}t${sep}t${sep}test.log`; + const events = test({ filename: (time: Date): string => (time ? rotated : filename), options: { size: "10B" } }, rfs => { + rfs.fsMkdir = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test " + path))); + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: [`test log${sep}t`], finish: 1, write: 1 })); + }); + + describe("error creating missing path (rotation)", () => { + const filename = `log${sep}t${sep}test.log`; + const rotated = `log${sep}t${sep}t${sep}test.log`; + const events = test({ filename: (time: Date): string => (time ? rotated : filename), options: { size: "10B" } }, rfs => { + rfs.on("rotation", () => (rfs.fsMkdir = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test " + path))))); + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: [`test log${sep}t${sep}t`], finish: 1, open: [filename], rotation: 1, write: 1, writev: 1 })); + }); + + describe("error creating second missing path", () => { + const filename = `log${sep}t${sep}test.log`; + const rotated = `log${sep}t${sep}t${sep}test.log`; + const events = test({ filename: (time: Date): string => (time ? rotated : filename), options: { size: "10B" } }, rfs => { + let first = true; + rfs.fsMkdir = (path: string, callback: (error: Error) => void): void => { + if(first) { + first = false; + return mkdir(path, callback); + } + process.nextTick(() => callback(new Error("test " + path))); + }; + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test log"], finish: 1, write: 1 })); + }); + + describe("error on no rotated file open", () => { + const filename = `log${sep}t${sep}test.log`; + const rotated = `log${sep}t${sep}t${sep}test.log`; + const events = test({ filename: (time: Date): string => (time ? rotated : filename), options: { size: "10B" } }, rfs => { + rfs.fsCreateWriteStream = (): any => ({ + end: (): void => {}, + once: (event: string, callback: (error: any) => void): any => (event === "error" ? setTimeout(() => callback({ code: "TEST" }), 50) : null) + }); + rfs.write("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["TEST"], finish: 1, write: 1 })); + }); + + describe("async error in sub stream", () => { + const events = test({ options: { size: "15B" } }, rfs => + rfs.once("open", () => { + rfs.stream.emit("error", new Error("test")); + rfs.write("test\n"); + }) + ); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], write: 1 })); + }); + + describe("error opening touched file", () => { + const events = test({ filename: "log/test.log", options: { size: "10B" } }, rfs => { + rfs.fsOpen = (path: string, flags: string, mode: number, callback: (error: any, fd?: number) => void): void => + process.nextTick(() => callback({ code: "ENOENT", message: `${path} ${flags} ${mode}` })); + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["ENOENT"], finish: 1, open: ["log/test.log"], rotation: 1, write: 1, writev: 1 })); + }); + + describe("error closing touched file", () => { + const events = test({ options: { size: "10B" } }, rfs => { + rfs.fsClose = (fd: number, callback: (error: Error) => void): void => close(fd, () => callback(new Error("test"))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error unlinking file", () => { + const events = test({ options: { size: "10B" } }, rfs => { + rfs.fsUnlink = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test " + path))); + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, warning: ["test 1-test.log"], write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("error in external compression function", () => { + const events = test( + { + options: { + compress: (): string => { + throw new Error("test"); + }, + rotate: 2, + size: "10B" + } + }, + rfs => rfs.write("test\ntest\n") + ); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error in stat (immutable)", () => { + const events = test({ options: { immutable: true, interval: "1d", size: "5B" } }, rfs => { + rfs.fsStat = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test " + path))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test 1-test.log"], finish: 1, write: 1 })); + }); + + describe("immutable exhausted", () => { + const events = test({ filename: () => "test.log", options: { immutable: true, interval: "1d", size: "5B" } }, rfs => rfs.write("test\n")); + + it("events", () => deq(events, { close: 1, error: ["RFS-TOO-MANY"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error in write stat", () => { + const events = test({ options: { size: "10B" } }, rfs => + rfs.once("open", () => { + rfs.fsStat = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(path + "test"))); + rfs.write("test\n"); + }) + ); + + it("events", () => deq(events, { close: 1, error: ["test.logtest"], finish: 1, open: ["test.log"], write: 1 })); + }); +}); diff --git a/test/05options.js b/test/05options.js deleted file mode 100644 index 7cadef6..0000000 --- a/test/05options.js +++ /dev/null @@ -1,291 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; -var utils = require("../utils"); - -describe("options", function() { - describe("size KiloBytes", function() { - before(function(done) { - this.rfs = rfs(done, { size: "10K" }); - this.rfs.end(); - }); - - it("10K", function() { - assert.equal(this.rfs.options.size, 10240); - }); - }); - - describe("size MegaBytes", function() { - before(function(done) { - this.rfs = rfs(done, { size: "10M" }); - this.rfs.end(); - }); - - it("10M", function() { - assert.equal(this.rfs.options.size, 10485760); - }); - }); - - describe("size GigaBytes", function() { - before(function(done) { - this.rfs = rfs(done, { size: "10G" }); - this.rfs.end(); - }); - - it("10G", function() { - assert.equal(this.rfs.options.size, 10737418240); - }); - }); - - describe("interval minutes", function() { - before(function(done) { - var self = this; - var doIt = function() { - self.rfs = rfs(done, { interval: "3m" }); - self.rfs.end(); - }; - - var now = new Date().getTime(); - var sec = parseInt(now / 1000, 10) * 1000; - - if(now - sec < 900) return doIt(); - - setTimeout(doIt, 101); - }); - - it("3'", function() { - assert.equal(this.rfs.options.interval.num, 3); - assert.equal(this.rfs.options.interval.unit, "m"); - }); - }); - - describe("interval hours", function() { - before(function(done) { - var self = this; - var doIt = function() { - self.rfs = rfs(done, { interval: "3h" }); - setTimeout(function() { - self.rfs._interval(new Date(2015, 2, 29, 1, 29, 23, 123).getTime()); - self.rfs.end(); - }, 30); - }; - - var now = new Date().getTime(); - var sec = parseInt(now / 1000, 10) * 1000; - - if(now - sec < 900) return doIt(); - - setTimeout(doIt, 101); - }); - - it("3h", function() { - assert.equal(this.rfs.options.interval.num, 3); - assert.equal(this.rfs.options.interval.unit, "h"); - }); - - it("hours daylight saving", function() { - assert.equal(this.rfs.next - this.rfs.prev, 7200000); - }); - }); - - describe("interval days", function() { - before(function(done) { - var self = this; - var doIt = function() { - self.rfs = rfs(done, { interval: "3d" }); - setTimeout(function() { - self.rfs._interval(new Date(2015, 2, 29, 1, 29, 23, 123).getTime()); - self.rfs.end(); - }, 30); - }; - - var now = new Date().getTime(); - var sec = parseInt(now / 1000, 10) * 1000; - - if(now - sec < 900) return doIt(); - - setTimeout(doIt, 101); - }); - - it("3d", function() { - assert.equal(this.rfs.options.interval.num, 3); - assert.equal(this.rfs.options.interval.unit, "d"); - }); - - it("days daylight saving", function() { - assert.equal(this.rfs.next - this.rfs.prev, 255600000); - }); - }); - - describe("path", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf /tmp/test.log /tmp/1-test.log ; echo test > /tmp/test.log", function() { - self.rfs = rfs(done, { path: "/tmp", size: "10B" }); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "/tmp/1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("/tmp/test.log"), ""); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("/tmp/1-test.log"), "test\ntest\n"); - }); - }); - - describe("safe options object", function() { - before(function(done) { - this.options = { size: "10M", interval: "30s", rotate: 5 }; - this.rfs = rfs(done, this.options); - this.rfs.end(); - }); - - it("10M", function() { - assert.equal(this.options.size, "10M"); - }); - - it("30s", function() { - assert.equal(this.options.interval, "30s"); - }); - - it("5 rotate", function() { - assert.equal(this.options.rotate, 5); - }); - }); - - describe("immutable", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > 1-test.log ; echo test > 2-test.log ; echo test >> 2-test.log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "10B" }); - self.rfs.ev.op = []; - self.rfs.on("open", function(filename) { - self.rfs.ev.op.push(filename); - }); - self.rfs.write("tes1\n"); - self.rfs.write("tes2\n"); - self.rfs.write("tes3\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("2 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 2); - }); - - it("2 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 2); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - assert.equal(this.rfs.ev.rotated[1], "3-test.log"); - }); - - it("3 open", function() { - assert.equal(this.rfs.ev.op.length, 3); - assert.equal(this.rfs.ev.op[0], "1-test.log"); - assert.equal(this.rfs.ev.op[1], "3-test.log"); - assert.equal(this.rfs.ev.op[2], "4-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("1st file content", function() { - assert.equal(fs.readFileSync("1-test.log"), "test\ntes1\n"); - }); - - it("2nd file content", function() { - assert.equal(fs.readFileSync("3-test.log"), "tes2\ntes3\n"); - }); - - it("3rd file content", function() { - assert.equal(fs.readFileSync("4-test.log"), "test\n"); - }); - }); - - describe("immutable with time", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { immutable: true, interval: "1d", size: "10B" }, utils.createGenerator("test.log")); - self.rfs.now = function() { - return new Date(1976, 0, 23, 13, 29, 23, 123).getTime(); - }; - self.rfs.ev.op = []; - self.rfs.on("open", function(filename) { - self.rfs.ev.op.push(filename); - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - }); - - it("2 open", function() { - assert.equal(this.rfs.ev.op.length, 2); - assert.equal(this.rfs.ev.op[1], "19760123-1329-02-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("1st file content", function() { - assert.equal(fs.readFileSync(this.rfs.ev.op[0]), "test\ntest\n"); - }); - - it("2nd file content", function() { - assert.equal(fs.readFileSync("19760123-1329-02-test.log"), "test\n"); - }); - }); -}); diff --git a/test/05options.ts b/test/05options.ts new file mode 100644 index 0000000..497ffeb --- /dev/null +++ b/test/05options.ts @@ -0,0 +1,137 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync } from "fs"; +import { sep } from "path"; +import { test } from "./helper"; + +describe("options", () => { + describe("size KiloBytes", () => { + let size: number; + const events = test({ options: { size: "10K" } }, rfs => rfs.end("test\n", () => (size = rfs.options.size))); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("10K", () => eq(size, 10240)); + }); + + describe("size MegaBytes", () => { + let size: number; + const events = test({ options: { size: "10M" } }, rfs => rfs.end("test\n", () => (size = rfs.options.size))); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("10M", () => eq(size, 10485760)); + }); + + describe("size GigaBytes", () => { + let size: number; + const events = test({ options: { size: "10G" } }, rfs => rfs.end("test\n", () => (size = rfs.options.size))); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("10G", () => eq(size, 10737418240)); + }); + + describe("interval minutes", () => { + let interval: number; + const events = test({ options: { interval: "3m" } }, rfs => rfs.end("test\n", () => (interval = rfs.options.interval))); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("3'", () => deq(interval, { num: 3, unit: "m" })); + }); + + describe("interval hours", () => { + let interval: number, next: number, prev: number; + const events = test({ options: { interval: "3h" } }, rfs => + rfs.end("test\n", () => { + interval = rfs.options.interval; + rfs.intervalBounds(new Date(2015, 2, 29, 1, 29, 23, 123)); + ({ next, prev } = rfs); + }) + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("3h", () => deq(interval, { num: 3, unit: "h" })); + it("hours daylight saving", () => eq(next - prev, 7200000)); + }); + + describe("interval days", () => { + let interval: number, next: number, prev: number; + const events = test({ options: { interval: "3d" } }, rfs => + rfs.end("test\n", () => { + interval = rfs.options.interval; + rfs.intervalBounds(new Date(2015, 2, 29, 1, 29, 23, 123)); + ({ next, prev } = rfs); + }) + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1 })); + it("3h", () => deq(interval, { num: 3, unit: "d" })); + it("hours daylight saving", () => eq(next - prev, 255600000)); + }); + + describe("path (ending)", () => { + const filename = `log${sep}test.log`; + const rotated = `log${sep}1-test.log`; + const events = test({ options: { path: "log" + sep, size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: [filename, filename], rotated: [rotated], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync(filename, "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync(rotated, "utf8"), "test\ntest\n")); + }); + + describe("path (not ending)", () => { + const filename = `log${sep}test.log`; + const rotated = `log${sep}1-test.log`; + const events = test({ options: { path: "log", size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: [filename, filename], rotated: [rotated], rotation: 1, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync(filename, "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync(rotated, "utf8"), "test\ntest\n")); + }); + + describe("safe options object", () => { + let options: any; + const events = test({ options: { size: "10M", interval: "1d", rotate: 5 } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + options = rfs.options; + }); + + it("options", () => deq(options, { interval: { num: 1, unit: "d" }, path: "", rotate: 5, size: 10485760 })); + it("events", () => deq(events, { finish: 1, open: ["test.log"], write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\ntest\ntest\n")); + }); + + describe("immutable", () => { + const events = test({ options: { immutable: true, interval: "1d", path: "log", size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["log/1-test.log", "log/2-test.log"], rotated: ["log/1-test.log"], rotation: 1, write: 1, writev: 1 })); + it("first file content", () => eq(readFileSync("log/1-test.log", "utf8"), "test\ntest\n")); + it("second file content", () => eq(readFileSync("log/2-test.log", "utf8"), "test\n")); + }); + + describe("immutable with file", () => { + const events = test({ files: { "1-test.log": "test\n" }, options: { immutable: true, interval: "1d", size: "10B" } }, rfs => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["1-test.log", "2-test.log", "3-test.log"], rotated: ["1-test.log", "2-test.log"], rotation: 2, write: 1, writev: 1 })); + it("first file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + it("second file content", () => eq(readFileSync("2-test.log", "utf8"), "test\ntest\n")); + it("third file content", () => eq(readFileSync("3-test.log", "utf8"), "")); + }); +}); diff --git a/test/06interval.js b/test/06interval.js deleted file mode 100644 index 61f7602..0000000 --- a/test/06interval.js +++ /dev/null @@ -1,261 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; -var utils = require("../utils"); - -describe("interval", function() { - describe("initial rotation with interval", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", interval: "1M" }, "test.log"); - self.rfs.now = function() { - return new Date(2015, 2, 29, 1, 29, 23, 123).getTime(); - }; - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "20150301-0000-01-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("20150301-0000-01-test.log"), "test\ntest\n"); - }); - }); - - describe("rotationTime option", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", interval: "1d", rotationTime: true, initialRotation: true }, function(time, index) { - self.time = time; - return utils.createGenerator("test.log")(time, index); - }); - self.rfs.now = function() { - return new Date(2015, 2, 29, 1, 29, 23, 123).getTime(); - }; - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - this.name = utils.createGenerator("test.log")(this.time, 1); - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], this.name); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync(this.name), "test\ntest\n"); - }); - }); - - describe("initialRotation option", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log ; touch -t 197601231500 test.log", function() { - self.rfs = rfs(done, { interval: "1m", initialRotation: true }, utils.createGenerator("test.log")); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "19760123-1500-01-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("19760123-1500-01-test.log"), "test\n"); - }); - }); - - describe("initialRotation option but ok", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { interval: "1m", initialRotation: true }, utils.createGenerator("test.log")); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("0 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 0); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\ntest\n"); - }); - }); - - describe("_write while rotation", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { interval: "1s" }); - self.rfs.once("rotation", self.rfs.end.bind(self.rfs, "test\n")); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - var cnt = fs.readFileSync("test.log").toString(); - assert.equal(cnt, "test\n"); - }); - - it("rotated file content", function() { - var cnt = fs.readFileSync("1-test.log").toString(); - assert.equal(cnt, "test\n"); - }); - }); - - describe("rotation while _write", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log ; echo test > test.log", function() { - self.rfs = rfs(done, { interval: "1s" }); - self.rfs.once("open", function() { - var stream = self.rfs.stream; - var prev = stream._write; - stream._write = function(chunk, encoding, callback) { - self.rfs.once("rotation", prev.bind(stream, chunk, encoding, callback)); - }; - - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - var cnt = fs.readFileSync("test.log").toString(); - assert.equal(cnt, "test\n"); - }); - - it("rotated file content", function() { - var cnt = fs.readFileSync("1-test.log").toString(); - assert.equal(cnt, "test\ntest\n"); - }); - }); -}); diff --git a/test/06interval.ts b/test/06interval.ts new file mode 100644 index 0000000..0968633 --- /dev/null +++ b/test/06interval.ts @@ -0,0 +1,125 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync } from "fs"; +import { test } from "./helper"; + +describe("interval", () => { + describe("initial rotation with interval", () => { + const events = test({ filename: "test.log", files: { "test.log": "test\ntest\n" }, options: { size: "10B", interval: "1M", intervalBoundary: true } }, rfs => { + rfs.now = (): Date => new Date(2015, 2, 29, 1, 29, 23, 123); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], rotated: ["20150301-0000-01-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("20150301-0000-01-test.log", "utf8"), "test\ntest\n")); + }); + + describe("intervalBoundary option", () => { + const events = test({ filename: "test.log", files: { "test.log": "test\n" }, options: { size: "10B", interval: "1d", initialRotation: true } }, rfs => { + rfs.now = (): Date => new Date(2015, 2, 29, 1, 29, 23, 123); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["20150329-0129-01-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("rotated file content", () => eq(readFileSync("20150329-0129-01-test.log", "utf8"), "test\ntest\n")); + }); + + describe("initialRotation option", () => { + const events = test( + { + filename: "test.log", + files: { "test.log": { content: "test\n", date: new Date(2015, 0, 23, 1, 29, 23, 123) } }, + options: { size: "10B", interval: "1d", intervalBoundary: true, initialRotation: true } + }, + rfs => { + rfs.now = (): Date => new Date(2015, 2, 29, 1, 29, 23, 123); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log"], rotated: ["20150123-0000-01-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("20150123-0000-01-test.log", "utf8"), "test\n")); + }); + + describe("initialRotation option but ok", () => { + const events = test( + { + filename: "test.log", + files: { "test.log": { content: "test\n", date: new Date(2015, 2, 29, 1, 0, 0, 0) } }, + options: { size: "10B", interval: "1d", intervalBoundary: true, initialRotation: true } + }, + rfs => { + rfs.now = (): Date => new Date(2015, 2, 29, 1, 29, 23, 123); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["20150329-0000-01-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("rotated file content", () => eq(readFileSync("20150329-0000-01-test.log", "utf8"), "test\ntest\n")); + }); + + describe("write while rotation", () => { + const events = test({ files: { "test.log": "test\ntest\n" }, options: { interval: "1s" } }, rfs => { + let ms = 990; + rfs.now = (): Date => new Date(2015, 0, 23, 0, 0, 0, (ms -= 10)); + rfs.once("rotation", () => rfs.end("test\n")); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("_write while rotation", () => { + const events = test({ files: { "test.log": "test\ntest\n" }, options: { interval: "1s" } }, rfs => { + const prev = rfs._write; + let ms = 990; + rfs.now = (): Date => new Date(2015, 0, 23, 0, 0, 0, (ms -= 10)); + rfs._write = (chunk: any, encoding: any, callback: any): any => rfs.once("rotation", prev.bind(rfs, chunk, encoding, callback)); + + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1-test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); + + describe("monthly rotation", () => { + const events = test({ files: { "test.log": "test\n" }, options: { interval: "2M", size: "10B" } }, rfs => { + let cnt = 0; + rfs.maxTimeout = 200; + rfs.now = (): Date => { + cnt++; + if(cnt === 1 || cnt === 2) return new Date(1976, 0, 23, 0, 0, 0, 0); + if(cnt === 3) return new Date(1976, 1, 1, 0, 0, 0, 0); + if(cnt === 4) return new Date(1976, 1, 29, 23, 59, 59, 950); + if(cnt === 5 || cnt === 6) return new Date(1976, 2, 1, 0, 0, 0, 0); + if(cnt === 7 || cnt === 8) return new Date(1976, 2, 10, 0, 0, 0, 0); + if(cnt === 9) return new Date(1976, 3, 30, 23, 59, 59, 950); + return new Date(1976, 4, 1, 0, 0, 0, 0); + }; + + rfs.write("test\n"); + rfs.once("rotated", (): void => { + rfs.write("test\n"); + rfs.write("test\n"); + rfs.once("rotated", (): void => { + rfs.write("test\n"); + rfs.once("rotated", (): void => { + rfs.end("test\n"); + }); + }); + }); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log", "test.log", "test.log"], rotated: ["1-test.log", "2-test.log", "3-test.log"], rotation: 3, write: 3, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\n")); + }); +}); diff --git a/test/07compression.js b/test/07compression.js deleted file mode 100644 index 5f2eda3..0000000 --- a/test/07compression.js +++ /dev/null @@ -1,558 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var cp = require("child_process"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("compression", function() { - describe("external", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(function() {}, { size: "10B", compress: true }, function(time, idx) { - if(time) return "test.log/" + idx; - return "test.log/log"; - }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - self.rfs.on("rotated", function() { - done(); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "test.log/1"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log/log"), ""); - }); - - it("rotated file content", function(done) { - cp.exec("zcat " + this.rfs.ev.rotated[0], function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - done(); - }); - }); - }); - - describe("external custom", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(setTimeout.bind(null, done, 100), { - size: "10B", - compress: function(source, dest) { - return "cat " + source + " | gzip -c9 > " + dest; - } - }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), ""); - }); - - it("rotated file content", function(done) { - cp.exec("zcat " + this.rfs.ev.rotated[0], function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - done(); - }); - }); - }); - - describe("internal (gzip)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(setTimeout.bind(null, done, 100), { size: "10B", compress: "gzip" }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "1-test.log"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), ""); - }); - - it("rotated file content", function(done) { - cp.exec("zcat " + this.rfs.ev.rotated[0], function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - done(); - }); - }); - }); - - describe("missing path", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(setTimeout.bind(null, done, 100), { size: "10B", compress: true }, function(time) { - if(time) return "log/test.log"; - return "test.log"; - }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "log/test.log"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), ""); - }); - - it("rotated file content", function(done) { - cp.exec("zcat " + this.rfs.ev.rotated[0], function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - done(); - }); - }); - }); - - describe("missing path (error)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - var mkdir = fs.mkdir; - fs.mkdir = function(path, callback) { - process.nextTick(callback.bind(null, { code: "EACCES" })); - }; - self.rfs = rfs( - function() { - fs.mkdir = mkdir; - done(); - }, - { size: "10B", compress: true }, - function(time) { - if(time) return "log/t/test.log"; - return "test.log"; - } - ); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "EACCES"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("missing path (error2)", function() { - before(function(done) { - var self = this; - var preO; - var preT; - exec(done, "rm -rf *log", function() { - self.rfs = rfs( - function() { - fs.open = preO; - done(); - }, - { size: "10B", compress: true } - ); - preT = self.rfs.touch; - self.rfs.touch = function(name, callback, retry) { - preO = fs.open; - fs.open = function(a, b, c) { - var e = new Error("test"); - e.code = "TEST"; - c(e); - }; - preT.call(this, name, callback, retry); - }; - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("can't find rotated file name", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B", compress: true }, function(time) { - if(time) return "index.js"; - return "test.log"; - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "RFS-TOO-MANY"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error creating tmp file", function() { - before(function(done) { - var self = this; - var preO = fs.open; - fs.open = function(path, flags, mode, cb) { - if(mode !== parseInt("777", 8)) return preO.apply(fs, arguments); - var e = new Error("test"); - fs.open = preO; - e.code = "TEST"; - cb(e); - }; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B", compress: true }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error writing tmp file", function() { - before(function(done) { - var self = this; - var preT; - var preW = fs.write; - exec(done, "rm -rf *log", function() { - self.rfs = rfs( - function() { - fs.write = preW; - done(); - }, - { size: "10B", compress: true } - ); - preT = self.rfs.touch; - self.rfs.touch = function(name, callback, retry) { - fs.write = function(a, b, c) { - var e = new Error("test"); - e.code = "TEST"; - c(e); - }; - preT.call(this, name, callback, retry); - }; - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error writing and closing tmp file", function() { - before(function(done) { - var self = this; - var preC = fs.close; - var preT; - var preW = fs.write; - var e = new Error("test"); - e.code = "TEST"; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B", compress: true }); - preT = self.rfs.touch; - self.rfs.touch = function(name, callback, retry) { - var done = false; - fs.close = function(a, b) { - if(done) { - fs.close = preC; - b(e); - } - else { - done = true; - preC(a, b); - } - }; - fs.write = function(a, b, c) { - fs.write = preW; - c(e); - }; - preT.call(this, name, callback, retry); - }; - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("Warning", function() { - assert.equal(this.rfs.ev.warn.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error closing tmp file", function() { - before(function(done) { - var self = this; - var preT; - var preC = fs.close; - var e = new Error("test"); - e.code = "TEST"; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B", compress: true }); - preT = self.rfs.touch; - self.rfs.touch = function(name, callback, retry) { - var done = false; - fs.close = function(a, b) { - if(done) { - fs.close = preC; - b(e); - } - else { - done = true; - preC(a, b); - } - }; - preT.call(this, name, callback, retry); - }; - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("error finding external tmp file", function() { - before(function(done) { - var self = this; - var preF; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { size: "10B", compress: true }); - preF = self.rfs.findName; - self.rfs.findName = function(att, tmp, callback) { - if(! ("1-test.log" in att)) return preF.apply(this, arguments); - var e = new Error("test"); - e.code = "TEST"; - callback(e); - }; - self.rfs.write("test\n"); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.code, "TEST"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); -}); diff --git a/test/07compression.ts b/test/07compression.ts new file mode 100644 index 0000000..86c1a58 --- /dev/null +++ b/test/07compression.ts @@ -0,0 +1,117 @@ +"use strict"; + +import { close, open, readFileSync, unlink } from "fs"; +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { gunzipSync } from "zlib"; +import { test } from "./helper"; + +describe("compression", () => { + describe("external", () => { + const events = test( + { + filename: (time: Date, index: number) => (time ? `test.log/${index}` : "test.log/log"), + options: { compress: true, size: "10B" } + }, + rfs => rfs.end("test\ntest\n") + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log/log", "test.log/log"], rotated: ["test.log/1"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log/log", "utf8"), "")); + it("rotated file content", () => eq(gunzipSync(readFileSync("test.log/1")).toString(), "test\ntest\n")); + }); + + describe("custom external", () => { + const events = test( + { + filename: (time: Date, index: number) => (time ? `test${index}.log` : "test.log"), + options: { compress: (source: string, dest: string): string => `cat ${source} | gzip -c9 > ${dest}`, size: "10B" } + }, + rfs => rfs.end("test\ntest\n") + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["test1.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("rotated file content", () => eq(gunzipSync(readFileSync("test1.log")).toString(), "test\ntest\n")); + }); + + describe("internal", () => { + const events = test({ filename: (time: Date, index: number): string => (time ? "log/log/test.gz" + index : "test.log"), options: { compress: "gzip", mode: 0o660, size: "10B" } }, rfs => + rfs.end("test\ntest\n") + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["log/log/test.gz1"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("rotated file content", () => eq(gunzipSync(readFileSync("log/log/test.gz1")).toString(), "test\ntest\n")); + }); + + describe("error finding external tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + const prev = rfs.findName; + rfs.findName = (tmp: boolean, callback: (error: Error) => void): void => { + rfs.findName = (tmp: boolean, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test"))); + prev.bind(rfs, tmp, callback)(); + }; + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error creating tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + rfs.fsOpen = (path: string, flags: string, mode: number, callback: (error: Error) => void): void => { + rfs.fsOpen = (path: string, flags: string, mode: number, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${path} ${flags} ${mode}`))); + open(path, flags, mode, callback); + }; + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test test.log.1.rfs.tmp w 511"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error writing tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + rfs.fsWrite = (fd: number, data: string, encoding: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${data} ${encoding}`))); + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test cat test.log | gzip -c9 > 1-test.log utf8"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error closing tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + rfs.fsClose = (fd: number, callback: (error: Error) => void): void => { + rfs.fsClose = (fd: number, callback: (error: Error) => void): void => close(fd, () => callback(new Error("test"))); + close(fd, callback); + }; + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error writing and closing tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + rfs.fsWrite = (fd: number, data: string, encoding: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${data} ${encoding}`))); + rfs.fsClose = (fd: number, callback: (error: Error) => void): void => { + rfs.fsClose = (fd: number, callback: (error: Error) => void): void => close(fd, () => callback(new Error("test"))); + close(fd, callback); + }; + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test cat test.log | gzip -c9 > 1-test.log utf8"], finish: 1, open: ["test.log"], rotation: 1, warning: ["test"], write: 1 })); + }); + + describe("error unlinking tmp file", () => { + const events = test({ options: { compress: true, size: "10B" } }, rfs => { + rfs.fsUnlink = (path: string, callback: (error: Error) => void): void => { + rfs.fsUnlink = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${path}`))); + unlink(path, callback); + }; + rfs.end("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test test.log"], finish: 1, open: ["test.log"], rotation: 1, warning: ["test ./test.log.1.rfs.tmp"], write: 1 })); + }); +}); diff --git a/test/08classical.ts b/test/08classical.ts new file mode 100644 index 0000000..a0e90be --- /dev/null +++ b/test/08classical.ts @@ -0,0 +1,132 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync, rename } from "fs"; +import { gunzipSync } from "zlib"; +import { sep } from "path"; +import { test } from "./helper"; + +describe("classical", function() { + describe("classical generator", () => { + const events = test({ filename: "test.log", options: { path: "log", rotate: 2, size: "10B" } }, rfs => { + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["log/test.log", "log/test.log"], rotated: ["log/test.log.1"], rotation: 1, write: 2 })); + it("file content", () => eq(readFileSync("log/test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("log/test.log.1", "utf8"), "test\ntest\n")); + }); + + describe("initial rotation with interval", () => { + const events = test( + { files: { "test.log": "test\ntest\n" }, filename: (index?: number): string => (index ? `${index}.test.log` : "test.log"), options: { interval: "1d", rotate: 2, size: "10B" } }, + rfs => { + rfs.write("test\n"); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["1.test.log", "2.test.log"], rotation: 2, write: 2 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "")); + it("first rotated file content", () => eq(readFileSync("1.test.log", "utf8"), "test\ntest\n")); + it("second rotated file content", () => eq(readFileSync("2.test.log", "utf8"), "test\ntest\n")); + }); + + describe("rotation overflow", () => { + const events = test({ filename: (index?: number): string => (index ? `${index}.test.log` : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\n"); + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log", "test.log", "test.log"], rotated: ["1.test.log", "2.test.log", "2.test.log"], rotation: 3, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("first rotated file content", () => eq(readFileSync("1.test.log", "utf8"), "test\ntest\n")); + it("second rotated file content", () => eq(readFileSync("2.test.log", "utf8"), "test\ntest\ntest\n")); + }); + + describe("missing directory", () => { + const events = test({ filename: (index?: number): string => (index ? `log${sep}${index}.test.log` : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log"], rotated: ["log/1.test.log"], rotation: 1, write: 2 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("rotated file content", () => eq(readFileSync("log/1.test.log", "utf8"), "test\ntest\n")); + }); + + describe("compression", () => { + const events = test({ filename: (index?: number): string => (index ? `${index}.test.log` : "test.log"), options: { compress: "gzip", rotate: 2, size: "10B" } }, rfs => { + rfs.write("test\ntest\ntest\n"); + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { finish: 1, open: ["test.log", "test.log", "test.log"], rotated: ["1.test.log", "2.test.log"], rotation: 2, write: 1, writev: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("first rotated file content", () => eq(gunzipSync(readFileSync("1.test.log")).toString(), "test\ntest\n")); + it("second rotated file content", () => eq(gunzipSync(readFileSync("2.test.log")).toString(), "test\ntest\ntest\n")); + }); + + describe("rotating on directory which is file", () => { + const events = test({ files: { txt: "test\n" }, filename: (index?: number): string => (index ? "txt/test.log" : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.write("test\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["ENOTDIR"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\ntest\n")); + }); + + describe("wrong name generator", () => { + const events = test( + { + filename: (index?: number): string => { + if(index) throw new Error("test"); + return "test.log"; + }, + options: { rotate: 2, size: "10B" } + }, + rfs => { + rfs.write("test\ntest\n"); + rfs.end("test\n"); + } + ); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\ntest\n")); + }); + + describe("first rename error", () => { + const events = test({ filename: (index?: number): string => (index ? "txt/test.log" : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.fsRename = (oldPath: string, newPath: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test"))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("mkdir error", () => { + const events = test({ filename: (index?: number): string => (index ? "txt/test.log" : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.fsMkdir = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test"))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("second rename error", () => { + const events = test({ filename: (index?: number): string => (index ? "txt/test.log" : "test.log"), options: { rotate: 2, size: "10B" } }, rfs => { + rfs.fsRename = (oldPath: string, newPath: string, callback: (error: Error) => void): void => { + rfs.fsRename = (oldPath: string, newPath: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error("test"))); + rename(oldPath, newPath, callback); + }; + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); +}); diff --git a/test/08use_case.js b/test/08use_case.js deleted file mode 100644 index e7ec802..0000000 --- a/test/08use_case.js +++ /dev/null @@ -1,201 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var cp = require("child_process"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("use cases", function() { - var pad = function(num) { - return (num > 9 ? "" : "0") + num; - }; - - describe("use case", function() { - before(function(done) { - var cnt = 0; - var self = this; - exec(done, "rm -rf *log *gz", function() { - self.rfs = rfs(done, { size: "10B", compress: true, interval: "1d" }, function(time, index) { - if(! time) return "test.log"; - - var year = time.getFullYear(); - var month = pad(time.getMonth() + 1); - var day = pad(time.getDate()); - - return year + "-" + month + "-" + day + "-test-" + pad(index) + ".log.gz"; - }); - var prev = self.rfs.now; - self.rfs.now = function() { - cnt++; - if(cnt === 1 || cnt === 2) return new Date(1976, 0, 23, 23, 59, 59, 700).getTime(); - if(cnt === 3 || cnt === 4) return new Date(1976, 0, 24, 23, 59, 59, 100).getTime(); - if(cnt === 5 || cnt === 6) return new Date(1976, 0, 24, 23, 59, 59, 200).getTime(); - return new Date(1976, 0, 25, 23, 59, 59, 100).getTime(); - }; - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("3 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 3); - }); - - it("3 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 3); - assert.equal(this.rfs.ev.rotated[0], "1976-01-23-test-01.log.gz"); - assert.equal(this.rfs.ev.rotated[1], "1976-01-24-test-01.log.gz"); - assert.equal(this.rfs.ev.rotated[2], "1976-01-24-test-02.log.gz"); - }); - - it("5 single write", function() { - assert.equal(this.rfs.ev.single, 5); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function(done) { - var self = this; - cp.exec("zcat " + self.rfs.ev.rotated[0], function(error, stdout, stderr) { - assert.equal(stdout, "test\n"); - cp.exec("zcat " + self.rfs.ev.rotated[1], function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - cp.exec("zcat " + self.rfs.ev.rotated[2], function(error, stdout, stderr) { - assert.equal(stdout, "test\n"); - done(); - }); - }); - }); - }); - }); - - describe("double makePath", function() { - before(function(done) { - var self = this; - var end = function() { - self.rfs1.end("test\n"); - }; - exec(done, "rm -rf *log", function() { - self.rfs1 = rfs(done, { path: "log/double", size: "15B" }, "test1.log"); - self.rfs2 = rfs(end, { path: "log/double", size: "15B" }, "test2.log"); - self.rfs2.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs1.ev.err); - assert.ifError(this.rfs2.ev.err); - }); - - it("0 rotation", function() { - assert.equal(this.rfs1.ev.rotation.length, 0); - assert.equal(this.rfs2.ev.rotation.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs1.ev.single, 1); - assert.equal(this.rfs2.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs1.ev.multi, 0); - assert.equal(this.rfs2.ev.multi, 0); - }); - - it("files content", function() { - assert.equal(fs.readFileSync("log/double/test1.log"), "test\n"); - assert.equal(fs.readFileSync("log/double/test2.log"), "test\n"); - }); - }); - - describe("monthly rotation", function() { - before(function(done) { - var cnt = 0; - var self = this; - exec(done, "rm -rf *log *gz", function() { - self.rfs = rfs(done, { size: "10B", interval: "2M" }, function(time, index) { - if(! time) return "test.log"; - - var year = time.getFullYear(); - var month = pad(time.getMonth() + 1); - var day = pad(time.getDate()); - - return year + "-" + month + "-" + day + "-test-" + pad(index) + ".log"; - }); - var prev = self.rfs.now; - self.rfs.maxTimeout = 200; - self.rfs.now = function() { - cnt++; - if(cnt === 1 || cnt === 2) return new Date(1976, 0, 23, 0, 0, 0, 0).getTime(); - if(cnt === 3) return new Date(1976, 1, 1, 0, 0, 0, 0).getTime(); - if(cnt === 4) return new Date(1976, 1, 29, 23, 59, 59, 950).getTime(); - if(cnt === 5 || cnt === 6) return new Date(1976, 2, 1, 0, 0, 0, 0).getTime(); - if(cnt === 7 || cnt === 8) return new Date(1976, 2, 10, 0, 0, 0, 0).getTime(); - if(cnt === 9) return new Date(1976, 3, 30, 23, 59, 59, 950).getTime(); - return new Date(1976, 4, 1, 0, 0, 0, 0).getTime(); - }; - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.write("test\n"); - self.rfs.once("rotated", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("3 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 3); - }); - - it("3 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 3); - assert.equal(this.rfs.ev.rotated[0], "1976-01-01-test-01.log"); - assert.equal(this.rfs.ev.rotated[1], "1976-03-01-test-01.log"); - assert.equal(this.rfs.ev.rotated[2], "1976-03-01-test-02.log"); - }); - - it("5 single write", function() { - assert.equal(this.rfs.ev.single, 5); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("files content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - assert.equal(fs.readFileSync("1976-01-01-test-01.log"), "test\n"); - assert.equal(fs.readFileSync("1976-03-01-test-01.log"), "test\ntest\n"); - assert.equal(fs.readFileSync("1976-03-01-test-02.log"), "test\n"); - }); - }); -}); diff --git a/test/09classical.js b/test/09classical.js deleted file mode 100644 index 1c73d6d..0000000 --- a/test/09classical.js +++ /dev/null @@ -1,432 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var cp = require("child_process"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; -var utils = require("../utils"); - -describe("classical", function() { - describe("initial rotation with interval", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log test.log.* ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", interval: "1d", rotate: 2 }, "test.log"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("1 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 1); - assert.equal(this.rfs.ev.rotated[0], "test.log.1"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content", function() { - assert.equal(fs.readFileSync("test.log.1"), "test\ntest\n"); - }); - }); - - describe("rotation overflow", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log test.log.* ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", rotate: 2 }, "test.log"); - self.rfs.write("test\ntest\n"); - self.rfs.write("test\ntest\ntest\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("3 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 3); - }); - - it("3 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 3); - assert.equal(this.rfs.ev.rotated[0], "test.log.1"); - assert.equal(this.rfs.ev.rotated[1], "test.log.2"); - assert.equal(this.rfs.ev.rotated[2], "test.log.2"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content 1", function() { - assert.equal(fs.readFileSync("test.log.1"), "test\ntest\ntest\n"); - }); - - it("rotated file content 2", function() { - assert.equal(fs.readFileSync("test.log.2"), "test\ntest\n"); - }); - }); - - describe("missing directory", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log test.log.* ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", rotate: 2 }, function(index) { - if(! index) return "test.log"; - return "test.log." + index + "/log"; - }); - self.rfs.write("test\ntest\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("2 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 2); - }); - - it("2 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 2); - assert.equal(this.rfs.ev.rotated[0], "test.log.1/log"); - assert.equal(this.rfs.ev.rotated[1], "test.log.2/log"); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content 1", function() { - assert.equal(fs.readFileSync("test.log.1/log"), "test\ntest\n"); - }); - - it("rotated file content 2", function() { - assert.equal(fs.readFileSync("test.log.2/log"), "test\ntest\n"); - }); - }); - - describe("compression", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log test.log.* ; echo test > test.log ; echo test >> test.log", function() { - self.rfs = rfs(done, { size: "10B", rotate: 2, compress: true }, "test.log"); - self.rfs.write("test\ntest\n"); - self.rfs.write("test\ntest\ntest\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("3 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 3); - }); - - it("3 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 3); - assert.equal(this.rfs.ev.rotated[0], "test.log.1"); - assert.equal(this.rfs.ev.rotated[1], "test.log.2"); - assert.equal(this.rfs.ev.rotated[2], "test.log.2"); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("1 multi write", function() { - assert.equal(this.rfs.ev.multi, 1); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("rotated file content 1", function(done) { - cp.exec("zcat test.log.1", function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\ntest\n"); - done(); - }); - }); - - it("rotated file content 2", function(done) { - cp.exec("zcat test.log.2", function(error, stdout, stderr) { - assert.equal(stdout, "test\ntest\n"); - done(); - }); - }); - }); - - describe("rotating on directory which is file", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log *.log.? ; > test2.log", function() { - self.rfs = rfs(done, { rotate: 2, size: "5B" }, function(count) { - if(count) return "test2.log/test.log"; - return "test.log"; - }); - self.rfs.write("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "ENOTDIR: not a directory, stat 'test2.log/test.log'"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("1 single write", function() { - assert.equal(this.rfs.ev.single, 1); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("wrong name generator (rotation)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { compress: "gzip", rotate: 1, size: "5B" }, function(count) { - if(count) throw new Error("test"); - return "test.log"; - }); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("exhausted (rotation)", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { compress: "gzip", rotate: 2, size: "5B" }, "test.log"); - var pre = self.rfs.findName; - self.rfs.findName = function(a, b, c) { - if(b) return c(Error("test")); - pre.apply(self, arguments); - }; - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("first rename error", function() { - var pre; - - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { rotate: 2, size: "5B" }, "test.log"); - pre = fs.rename; - fs.rename = function(a, b, c) { - if(a === "test.log" && b === "test.log.1") return c(Error("test")); - pre.apply(fs, arguments); - }; - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - after(function() { - fs.rename = pre; - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("makePath", function() { - var pre; - - before(function(done) { - var self = this; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { rotate: 2, size: "5B" }, function(count) { - if(count) return "test2.log/test.log"; - return "test.log"; - }); - pre = utils.makePath; - utils.makePath = function(a, c) { - c(Error("test")); - }; - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - after(function() { - utils.makePath = pre; - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); - - describe("second rename error", function() { - var pre; - - before(function(done) { - var self = this; - var count = 0; - exec(done, "rm -rf *log", function() { - self.rfs = rfs(done, { rotate: 2, size: "5B" }, function(count) { - if(count) return "test2.log/test.log"; - return "test.log"; - }); - pre = fs.rename; - fs.rename = function(a, b, c) { - if(a === "test.log" && b === "test2.log/test.log" && ++count === 2) return c(Error("test")); - pre.apply(fs, arguments); - }; - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - after(function() { - fs.rename = pre; - }); - - it("Error", function() { - assert.equal(this.rfs.err.message, "test"); - }); - - it("1 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 1); - }); - - it("0 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 0); - }); - - it("2 single write", function() { - assert.equal(this.rfs.ev.single, 2); - }); - - it("0 multi write", function() { - assert.equal(this.rfs.ev.multi, 0); - }); - }); -}); diff --git a/test/09history.ts b/test/09history.ts new file mode 100644 index 0000000..a2ce7b1 --- /dev/null +++ b/test/09history.ts @@ -0,0 +1,174 @@ +"use strict"; + +import { deepStrictEqual as deq, strictEqual as eq } from "assert"; +import { readFileSync } from "fs"; +import { test } from "./helper"; + +describe("history", () => { + describe("maxFiles", () => { + const events = test({ files: { "log/files.txt": "test\nnone\n" }, options: { history: "files.txt", maxFiles: 3, path: "log", size: "10B" } }, rfs => { + rfs.write("test\ntest\n"); + rfs.write("test\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\ntest\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => + deq(events, { + finish: 1, + history: 5, + open: ["log/test.log", "log/test.log", "log/test.log", "log/test.log", "log/test.log", "log/test.log"], + removedn: ["log/1-test.log", "log/2-test.log"], + rotated: ["log/1-test.log", "log/2-test.log", "log/3-test.log", "log/4-test.log", "log/1-test.log"], + rotation: 5, + warning: ["File 'test' contained in history is not a regular file"], + write: 1, + writev: 1 + })); + it("file content", () => eq(readFileSync("log/test.log", "utf8"), "test\n")); + it("first rotated file content", () => eq(readFileSync("log/3-test.log", "utf8"), "test\ntest\ntest\ntest\n")); + it("second rotated file content", () => eq(readFileSync("log/4-test.log", "utf8"), "test\ntest\ntest\ntest\ntest\n")); + it("third rotated file content", () => eq(readFileSync("log/1-test.log", "utf8"), "test\ntest\ntest\ntest\ntest\ntest\n")); + it("history file content", () => eq(readFileSync("log/files.txt", "utf8"), "log/3-test.log\nlog/4-test.log\nlog/1-test.log\n")); + }); + + describe("maxSize", () => { + const events = test({ options: { maxSize: "60B", size: "10B" } }, rfs => { + rfs.write("test\ntest\n"); + rfs.write("test\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\ntest\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => + deq(events, { + finish: 1, + history: 5, + open: ["test.log", "test.log", "test.log", "test.log", "test.log", "test.log"], + removeds: ["1-test.log", "2-test.log", "3-test.log"], + rotated: ["1-test.log", "2-test.log", "3-test.log", "4-test.log", "1-test.log"], + rotation: 5, + write: 1, + writev: 1 + })); + it("file content", () => eq(readFileSync("test.log", "utf8"), "test\n")); + it("first rotated file content", () => eq(readFileSync("4-test.log", "utf8"), "test\ntest\ntest\ntest\ntest\n")); + it("second rotated file content", () => eq(readFileSync("1-test.log", "utf8"), "test\ntest\ntest\ntest\ntest\ntest\n")); + }); + + describe("error reading history file", () => { + const events = test({ options: { maxSize: "60B", size: "10B" } }, rfs => { + rfs.fsReadFile = (path: string, encoding: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${path} ${encoding}`))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test test.log.txt utf8"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error writing history file", () => { + const events = test({ options: { maxSize: "60B", size: "10B" } }, rfs => { + rfs.fsWriteFile = (path: string, data: string, encoding: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${path} ${data} ${encoding}`))); + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test test.log.txt 1-test.log\n utf8"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error checking rotated file", () => { + const events = test({ options: { maxSize: "60B", size: "10B" } }, rfs => { + const prev = rfs.history; + rfs.history = (filename: string, callback: (error: Error) => void): void => { + rfs.fsStat = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${path}`))); + prev.bind(rfs, filename, callback)(); + }; + rfs.write("test\ntest\n"); + }); + + it("events", () => deq(events, { close: 1, error: ["test 1-test.log"], finish: 1, open: ["test.log"], rotation: 1, write: 1 })); + }); + + describe("error removing rotated file (size)", () => { + const events = test({ options: { maxSize: "50B", size: "10B" } }, rfs => { + const prev = rfs.historyRemove; + rfs.historyRemove = (files: any, size: boolean, callback: (error: Error) => void): void => { + rfs.fsUnlink = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${files.map((e: any) => e.name).join(", ")} ${size}`))); + prev.bind(rfs, files, size, callback)(); + }; + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + }); + + it("events", () => + deq(events, { + close: 1, + error: ["test 2-test.log, 3-test.log true"], + finish: 1, + history: 2, + open: ["test.log", "test.log", "test.log"], + rotation: 3, + rotated: ["1-test.log", "2-test.log"], + write: 1, + writev: 1 + })); + }); + + describe("error removing rotated file (files)", () => { + const events = test({ options: { maxFiles: 2, size: "10B" } }, rfs => { + const prev = rfs.historyRemove; + rfs.historyRemove = (files: any, size: boolean, callback: (error: Error) => void): void => { + rfs.fsUnlink = (path: string, callback: (error: Error) => void): void => process.nextTick(() => callback(new Error(`test ${files.map((e: any) => e.name).join(", ")} ${size}`))); + prev.bind(rfs, files, size, callback)(); + }; + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + }); + + it("events", () => + deq(events, { + close: 1, + error: ["test 2-test.log, 3-test.log false"], + finish: 1, + history: 2, + open: ["test.log", "test.log", "test.log"], + rotation: 3, + rotated: ["1-test.log", "2-test.log"], + write: 1, + writev: 1 + })); + }); + + describe("immutable", () => { + let min = 0; + const events = test({ filename: "test.log", options: { immutable: true, interval: "1d", maxFiles: 2, size: "10B" } }, rfs => { + rfs.now = (): Date => new Date(2015, 0, 23, 1, ++min, 23, 123); + rfs.write("test\ntest\n"); + rfs.write("test\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\n"); + rfs.write("test\ntest\ntest\ntest\ntest\n"); + rfs.end("test\n"); + }); + + it("events", () => + deq(events, { + finish: 1, + history: 4, + open: ["20150123-0101-01-test.log", "20150123-0105-01-test.log", "20150123-0109-01-test.log", "20150123-0113-01-test.log", "20150123-0117-01-test.log"], + removedn: ["20150123-0101-01-test.log", "20150123-0105-01-test.log"], + rotated: ["20150123-0101-01-test.log", "20150123-0105-01-test.log", "20150123-0109-01-test.log", "20150123-0113-01-test.log"], + rotation: 4, + write: 1, + writev: 1 + })); + it("file content", () => eq(readFileSync("20150123-0117-01-test.log", "utf8"), "test\n")); + it("first rotated file content", () => eq(readFileSync("20150123-0109-01-test.log", "utf8"), "test\ntest\ntest\ntest\n")); + it("second rotated file content", () => eq(readFileSync("20150123-0113-01-test.log", "utf8"), "test\ntest\ntest\ntest\ntest\n")); + }); +}); diff --git a/test/10history.js b/test/10history.js deleted file mode 100644 index 3b57289..0000000 --- a/test/10history.js +++ /dev/null @@ -1,377 +0,0 @@ -"use strict"; - -var assert = require("assert"); -var exec = require("./helper").exec; -var fs = require("fs"); -var rfs = require("./helper").rfs; - -describe("history", function() { - describe("maxFiles", function() { - before(function(done) { - var self = this; - var end = doneN(done, 2); - exec(done, "rm -rf *log *txt ; echo none > test.log.txt ; echo -n test >> test.log.txt", function() { - self.rfs = rfs(end, { size: "10B", maxFiles: 3 }); - self.rfs.on("removed", function(name, number) { - self.removed = name; - self.number = number; - end(); - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn, "File 'test' contained in history is not a regular file"); - }); - - it("4 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 4); - }); - - it("4 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 4); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("removed", function() { - assert.equal(this.removed, "1-test.log"); - assert.equal(this.number, true); - }); - - it("removed first rotated file", function() { - assert.equal(fs.existsSync("1-test.log"), false); - }); - - it("second rotated file content", function() { - assert.equal(fs.readFileSync("2-test.log"), "test\ntest\n"); - }); - - it("third rotated file content", function() { - assert.equal(fs.readFileSync("3-test.log"), "test\ntest\n"); - }); - - it("forth rotated file content", function() { - assert.equal(fs.readFileSync("4-test.log"), "test\ntest\n"); - }); - }); - - describe("maxSize", function() { - before(function(done) { - var self = this; - var end = doneN(done, 2); - exec(done, "rm -rf *log", function() { - self.rfs = rfs(end, { size: "10B", maxSize: "35B", history: "history.log" }); - self.rfs.on("removed", function(name, number) { - self.removed = name; - self.number = number; - end(); - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("4 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 4); - }); - - it("4 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 4); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("test.log"), "test\n"); - }); - - it("removed", function() { - assert.equal(this.removed, "1-test.log"); - assert.equal(this.number, false); - }); - - it("removed first rotated file", function() { - assert.equal(fs.existsSync("1-test.log"), false); - }); - - it("second rotated file content", function() { - assert.equal(fs.readFileSync("2-test.log"), "test\ntest\n"); - }); - - it("third rotated file content", function() { - assert.equal(fs.readFileSync("3-test.log"), "test\ntest\n"); - }); - - it("forth rotated file content", function() { - assert.equal(fs.readFileSync("4-test.log"), "test\ntest\n"); - }); - }); - - describe("error reading history file", function() { - before(function(done) { - var self = this; - exec(done, "rm -rf *log *txt", function() { - self.rfs = rfs(setTimeout.bind(null, done, 100), { size: "10B", maxFiles: 1, history: "test" }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn.code, "EISDIR"); - }); - }); - - describe("error writing history file", function() { - before(function(done) { - var self = this; - var pre = fs.writeFile; - var end = doneN(done, 2); - fs.writeFile = function(a, b, c, d) { - d("TEST"); - }; - exec(done, "rm -rf *log *txt", function() { - self.rfs = rfs(end, { size: "10B", maxFiles: 1 }); - self.rfs.on("removed", function(name) { - self.removed = name; - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.end("test\n"); - self.rfs.once("warning", function() { - fs.writeFile = pre; - end(); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn, "TEST"); - }); - }); - - describe("error removing file", function() { - before(function(done) { - var self = this; - var pre = fs.unlink; - var end = doneN(done, 2); - fs.unlink = function(a, b) { - fs.unlink = pre; - b("TEST"); - }; - exec(done, "rm -rf *log *txt", function() { - self.rfs = rfs(end, { size: "10B", maxFiles: 1 }); - self.rfs.on("removed", function(name) { - self.removed = name; - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("warning", end); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn, "TEST"); - }); - }); - - describe("error checking file", function() { - before(function(done) { - var self = this; - var preR = fs.readFile; - var preS = fs.stat; - exec(done, "rm -rf *log *txt", function() { - self.rfs = rfs(setTimeout.bind(null, done, 100), { size: "10B", maxFiles: 1 }); - self.rfs.on("removed", function(name) { - self.removed = name; - }); - self.rfs.on("warning", function() { - self.rfs.end("test\n"); - }); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - fs.readFile = function(a, b, c) { - fs.stat = function(a, b) { - fs.stat = preS; - b("TEST"); - }; - fs.readFile = preR; - fs.readFile(a, b, c); - }; - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn, "TEST"); - }); - }); - - describe("immutable", function() { - var last; - - before(function(done) { - var self = this; - var end = doneN(done, 3); - var min = 0; - exec(done, "rm -rf *log *txt ; echo none > test.log.txt ; echo -n test >> test.log.txt", function() { - self.rfs = rfs(end, { immutable: true, interval: "1d", size: "10B", maxFiles: 3 }, function(time, idx) { - if(! time) return "test.log"; - var pad = function(num) { - return (num > 9 ? "" : "0") + num; - }; - var month = time.getFullYear() + "" + pad(time.getMonth() + 1); - var day = pad(time.getDate()); - var hour = pad(time.getHours()); - var minute = pad(time.getMinutes()); - - return month + day + "-" + hour + minute + "-" + idx + "-test.log"; - }); - self.rfs.now = function() { - return new Date(2015, 0, 23, 1, ++min, 23, 123).getTime(); - }; - self.rfs.on("removed", function(name, number) { - self.removed = name; - self.number = number; - end(); - }); - self.rfs.on("open", function(name) { - last = name; - }); - //setTimeout(done, 1800); - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.write("test\n"); - self.rfs.write("test\n"); - self.rfs.once("history", function() { - self.rfs.end("test\n"); - }); - }); - }); - }); - }); - }); - }); - - it("no error", function() { - assert.ifError(this.rfs.ev.err); - }); - - it("warning", function() { - assert.equal(this.rfs.ev.warn, "File 'test' contained in history is not a regular file"); - }); - - it("5 rotation", function() { - assert.equal(this.rfs.ev.rotation.length, 5); - }); - - it("5 rotated", function() { - assert.equal(this.rfs.ev.rotated.length, 5); - }); - - it("file content", function() { - assert.equal(fs.readFileSync("20150123-0116-1-test.log"), "test\n"); - }); - - it("removed", function() { - assert.equal(this.removed, "20150123-0107-1-test.log"); - assert.equal(this.number, true); - }); - - it("removed first rotated file", function() { - assert.equal(fs.existsSync("20150123-0107-1-test.log"), false); - }); - - it("third rotated file content", function() { - assert.equal(fs.readFileSync("20150123-0110-1-test.log"), "test\ntest\n"); - }); - - it("forth rotated file content", function() { - assert.equal(fs.readFileSync("20150123-0113-1-test.log"), "test\ntest\n"); - }); - - it("last file", function() { - assert.equal( - fs - .readFileSync("test.log.txt") - .toString() - .split("\n")[2], - last - ); - }); - }); -}); diff --git a/test/99clean.js b/test/99clean.js deleted file mode 100644 index 7a33d47..0000000 --- a/test/99clean.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -var cp = require("child_process"); - -describe("clean", function() { - it("clean", function() { - cp.exec("rm -rf *log *gz *tmp *txt", function() {}); - }); -}); diff --git a/test/99clean.ts b/test/99clean.ts new file mode 100644 index 0000000..83ff27d --- /dev/null +++ b/test/99clean.ts @@ -0,0 +1,10 @@ +"use strict"; + +import { deepStrictEqual as deq } from "assert"; +import { test } from "./helper"; + +describe("clean", () => { + const events = test({ filename: "test" }, rfs => rfs.end("test")); + + it("clean", () => deq(events, { close: 1, error: ["Can't write on: test (it is not a file)"], finish: 1, write: 1 })); +}); diff --git a/test/helper.js b/test/helper.js deleted file mode 100644 index c89c5ea..0000000 --- a/test/helper.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; - -var cp = require("child_process"); -var rfs = require(".."); - -function exec(done, cmd, cb) { - cp.exec(cmd, function(error, stdout, stderr) { - if(error) { - console.log(error, stdout, stderr); - - return done(); - } - - cb(); - }); -} - -function _rfs(done, options, generator) { - var ret = rfs( - generator || - function(time, index) { - if(time) return index + "-test.log"; - return "test.log"; - }, - options - ); - - ret.ev = { single: 0, multi: 0, rotation: [], rotated: [] }; - ret.on("rotation", function(filename) { - ret.ev.rotation.push(filename); - }); - ret.on("rotated", function(filename) { - ret.ev.rotated.push(filename); - }); - ret.once("warning", function(err) { - ret.ev.warn = err; - }); - ret.on("finish", done); - - var oldw = ret._write; - var oldv = ret._writev; - - ret._write = function(chunk, encoding, callback) { - ret.ev.single++; - oldw.call(ret, chunk, encoding, callback); - }; - - ret._writev = function(chunks, callback) { - ret.ev.multi++; - oldv.call(ret, chunks, callback); - }; - - return ret; -} - -module.exports = { - exec: exec, - rfs: _rfs -}; - -global.doneN = function(done, num) { - var n = 0; - - return function() { - if(++n === num) done(); - }; -}; - -// eslint-disable-next-line no-extend-native -Array.prototype.test = () => {}; diff --git a/test/helper.ts b/test/helper.ts new file mode 100644 index 0000000..39968ea --- /dev/null +++ b/test/helper.ts @@ -0,0 +1,145 @@ +"use strict"; + +import { RotatingFileStream, createStream } from ".."; +import { Stats, close, futimes, mkdir, open, readdir, rmdir, stat, unlink, write } from "fs"; +import { sep } from "path"; + +const proto: any = RotatingFileStream.prototype; +const pseudo = { fsMkdir: mkdir, makePath: proto.makePath }; + +function fillFiles(files: any, done: () => void): void { + if(! files) return done(); + + let empty = 0; + let filled = 0; + + const end = (): void => (++filled === empty ? done() : null); + + Object.keys(files).map((file: string) => { + const value = files[file]; + const content = typeof value === "string" ? value : value.content; + + const fill = (fd: number): void => + write(fd, content, (error: Error): any => { + if(error) return process.stderr.write(`Error writing on '${file}': ${error.message}\n`, end); + const done = (): void => + close(fd, (error: Error): any => { + if(error) return process.stderr.write(`Error closing for '${file}': ${error.message}\n`, end); + end(); + }); + if(typeof value !== "object" || ! value.date) return done(); + futimes(fd, files[file].date, files[file].date, (error: Error): any => { + if(error) return process.stderr.write(`Error changing date for '${file}': ${error.message}\n`, end); + done(); + }); + }); + + const reopen = (file: string, retry?: boolean): void => + open(file, "w", (error: NodeJS.ErrnoException, fd: number): any => { + if(error) { + if(error.code === "ENOENT" && ! retry) return pseudo.makePath(file, () => reopen(file, true)); + return process.stderr.write(`Error opening '${file}': ${error.message}\n`, end); + } + fill(fd); + }); + + ++empty; + reopen(file); + }); + + if(empty === 0) done(); +} + +function recursiveRemove(path: string, done: () => any): any { + const notRoot: boolean = path !== "."; + + stat(path, (error: Error, stats: Stats): any => { + const rm = (): void => (notRoot ? (stats.isFile() ? unlink : rmdir)(path, error => (error ? process.stderr.write(`Error deleting '${path}': ${error.message}\n`, done) : done())) : done()); + + if(error) return process.stderr.write(`Error getting stats for '${path}': ${error.message}\n`, done); + if(stats.isFile()) return rm(); + if(! stats.isDirectory()) return process.stderr.write(`'${path}': Unknown file type`, done); + + readdir(path, (error, files) => { + if(error) return process.stderr.write(`Error reading dir '${path}': ${error.message}\n`, done); + + let count = 0; + let total = 0; + + const callback: () => void = () => (++count === total ? rm() : null); + + files.map(file => { + if(notRoot || file.match(/(gz|log|tmp|txt)$/)) { + total++; + recursiveRemove(path + sep + file, callback); + } + }); + + if(total === 0) rm(); + }); + }); +} + +export function test(opt: any, test: (rfs: any) => void): any { + const { filename, files, options } = opt; + const events: any = {}; + + before(function(done): void { + let did: boolean; + + const generator = filename ? filename : (time: Date, index?: number): string => (time ? index + "-test.log" : "test.log"); + const timeOut = setTimeout(() => { + events.timedOut = true; + done(); + }, this.timeout() - 500); + + const end = (): void => { + clearTimeout(timeOut); + if(did) return; + did = true; + done(); + }; + + const create = (): void => { + const rfs = createStream(generator, options); + const inc: (name: string) => any = name => { + if(! events[name]) events[name] = 0; + events[name]++; + }; + const push: (name: string, what: string) => any = (name, what) => { + if(! events[name]) events[name] = []; + events[name].push(what); + }; + + rfs.on("close", () => inc("close")); + rfs.on("error", error => push("error", "code" in error ? error["code"] : error.message)); + rfs.on("finish", end); + rfs.on("finish", () => inc("finish")); + rfs.on("history", () => inc("history")); + rfs.on("open", filename => push("open", filename)); + rfs.on("removed", (filename, number) => push("removed" + (number ? "n" : "s"), filename)); + rfs.on("rotated", filename => push("rotated", filename)); + rfs.on("rotation", () => inc("rotation")); + rfs.on("warning", error => push("warning", error.message)); + + const oldw = rfs._write; + const oldv = rfs._writev; + + rfs._write = (chunk: Buffer, encoding: string, callback: (error?: Error) => void): void => { + inc("write"); + oldw.call(rfs, chunk, encoding, callback); + }; + + rfs._writev = (chunks: any, callback: (error?: Error) => void): void => { + inc("writev"); + oldv.call(rfs, chunks, callback); + }; + + test(rfs); + }; + + recursiveRemove(".", () => fillFiles(files, create)); + }); + + return events; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c80b7d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "commonjs", + "target": "es6" + }, + "include": ["./index.ts"] +} diff --git a/utils.js b/utils.js deleted file mode 100644 index f676bb2..0000000 --- a/utils.js +++ /dev/null @@ -1,234 +0,0 @@ -"use strict"; - -var fs = require("fs"); -var path = require("path"); - -function buildNumberCheck(field) { - return function(typ, options, val) { - var value = parseInt(val, 10); - - if(value !== val || value <= 0 || typ !== "number") throw new Error("'" + field + "' option must be a positive integer number"); - }; -} - -function buildStringCheck(field, check) { - return function(typ, options, val) { - if(typ !== "string") throw new Error("Don't know how to handle 'options." + field + "' type: " + typ); - - options[field] = check(val); - }; -} - -function checkMeasure(v, what, units) { - var ret = {}; - - ret.num = parseInt(v, 10); - - if(isNaN(ret.num)) throw new Error("Unknown 'options." + what + "' format: " + v); - - if(ret.num <= 0) throw new Error("A positive integer number is expected for 'options." + what + "'"); - - ret.unit = v.replace(/^[ 0]*/g, "").substr((ret.num + "").length, 1); - - if(ret.unit.length === 0) throw new Error("Missing unit for 'options." + what + "'"); - - if(! units[ret.unit]) throw new Error("Unknown 'options." + what + "' unit: " + ret.unit); - - return ret; -} - -var intervalUnits = { - M: true, - d: true, - h: true, - m: true, - s: true -}; - -function checkIntervalUnit(ret, unit, amount) { - if(parseInt(amount / ret.num, 10) * ret.num !== amount) throw new Error("An integer divider of " + amount + " is expected as " + unit + " for 'options.interval'"); -} - -function checkInterval(v) { - var ret = checkMeasure(v, "interval", intervalUnits); - - switch(ret.unit) { - case "h": - checkIntervalUnit(ret, "hours", 24); - break; - - case "m": - checkIntervalUnit(ret, "minutes", 60); - break; - - case "s": - checkIntervalUnit(ret, "seconds", 60); - break; - } - - return ret; -} - -var sizeUnits = { - B: true, - G: true, - K: true, - M: true -}; - -function checkSize(v) { - var ret = checkMeasure(v, "size", sizeUnits); - - if(ret.unit === "K") return ret.num * 1024; - - if(ret.unit === "M") return ret.num * 1048576; - - if(ret.unit === "G") return ret.num * 1073741824; - - return ret.num; -} - -var checks = { - compress: function(typ, options, val) { - if(! val) throw new Error("A value for 'options.compress' must be specified"); - - if(typ === "boolean") - options.compress = function(src, dst) { - return "cat " + src + " | gzip -c9 > " + dst; - }; - else if(typ === "string") { - //if(val != "bzip" && val != "gzip") - if(val !== "gzip") throw new Error("Don't know how to handle compression method: " + val); - } - else if(typ !== "function") throw new Error("Don't know how to handle 'options.compress' type: " + typ); - }, - - highWaterMark: function() {}, - - history: function(typ) { - if(typ !== "string") throw new Error("Don't know how to handle 'options.history' type: " + typ); - }, - - immutable: function() {}, - - initialRotation: function() {}, - - interval: buildStringCheck("interval", checkInterval), - - maxFiles: buildNumberCheck("maxFiles"), - - maxSize: buildStringCheck("maxSize", checkSize), - - mode: function() {}, - - path: function(typ) { - if(typ !== "string") throw new Error("Don't know how to handle 'options.path' type: " + typ); - }, - - rotate: buildNumberCheck("rotate"), - - rotationTime: function() {}, - - size: buildStringCheck("size", checkSize) -}; - -function checkOptions(options) { - if(! options) return {}; - - if(typeof options !== "object") throw new Error("Don't know how to handle 'options' type: " + typeof options); - - var ret = {}; - - for(var opt in options) { - var val = options[opt]; - var typ = typeof val; - - if(! (opt in checks)) throw new Error("Unknown option: " + opt); - - ret[opt] = options[opt]; - checks[opt](typ, ret, val); - } - - if(! ret.interval) { - delete ret.immutable; - delete ret.initialRotation; - delete ret.rotationTime; - } - - if(ret.rotate) { - delete ret.history; - delete ret.immutable; - delete ret.maxFiles; - delete ret.maxSize; - delete ret.rotationTime; - } - - if(ret.immutable) delete ret.compress; - - if(ret.rotationTime) delete ret.initialRotation; - - return ret; -} - -function pad(num) { - return (num > 9 ? "" : "0") + num; -} - -function createClassical(filename) { - return function(index) { - if(! index) return filename; - - return filename + "." + index; - }; -} - -function createGenerator(filename) { - return function(time, index) { - if(! time) return filename; - - var month = time.getFullYear() + "" + pad(time.getMonth() + 1); - var day = pad(time.getDate()); - var hour = pad(time.getHours()); - var minute = pad(time.getMinutes()); - - return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename; - }; -} - -function makePath(name, callback) { - var dir = path.parse(name).dir; - - fs.mkdir(dir, function(e) { - if(e) { - if(e.code === "ENOENT") return makePath(dir, callback); - - if(e.code !== "EEXIST") return callback(e); - } - - callback(); - }); -} - -function setEvents(self) { - self.once("error", function(err) { - self.err = err; - self.end(); - }); - - self.once("finish", self._clear.bind(self)); - - self.on("rotated", function() { - self.rotation = null; - self._rewrite(); - }); - - if((self.options.maxFiles || self.options.maxSize) && ! self.options.rotate) self.on(self.options.immutable ? "open" : "rotated", self.history.bind(self)); -} - -module.exports = { - checkOptions: checkOptions, - createClassical: createClassical, - createGenerator: createGenerator, - makePath: makePath, - setEvents: setEvents -}; diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..5d030d0 --- /dev/null +++ b/utils.ts @@ -0,0 +1,52 @@ +"use strict"; + +import { readFile, unlink, writeFile } from "fs"; + +const common: string[] = ["*gz", "*log", "*tmp", "*txt", ".gitignore", ".npmignore", ".nyc_output", "coverage", "node_modules", ""]; +const git: string[] = ["index.d.ts", "index.js"]; +const npm: string[] = [".*", "index.ts", "test", "tsconfig.json", "tslint.json", "utils.ts"]; + +const readme: (err: Error, data: string) => void = (err, data) => { + if(err) return process.stderr.write(`Error reading README.md: ${err.message}`); + + const input = data.split("\n"); + + readFile("index.d.ts", "utf8", (err: Error, data: string) => { + if(err) return process.stderr.write(`Error reading index.d.ts: ${err.message}`); + + const output = []; + let begin: boolean, end: boolean; + + input.map((line: string) => { + if(begin) { + if(end) output.push(line); + else if(line === "```") { + output.push(line); + end = true; + } + } + + if(! begin) { + output.push(line); + if(line === "```typescript") { + let first: boolean, interf: boolean; + begin = true; + data.split("\n").map((line: string) => { + if(! first) return (first = true); + if(interf) return (interf = ! line.match(/}$/)); + if(line.match(/interface Chunk/) || line.match(/interface Opts/)) return (interf = true); + if(line.match(/class RotatingFileStream/)) interf = ((line = line.replace("{", "{}")) as unknown) as boolean; + if(! line.match(/Callback/) && ! line.match(/export \{}/)) output.push(line.replace(" ", " ")); + }); + output.pop(); + } + } + }); + + writeFile("README.md", output.join("\n"), () => {}); + }); +}; + +if(process.argv[2] === "clean") unlink("index.js", (): void => unlink("index.d.ts", (): void => {})); +if(process.argv[2] === "ignore") writeFile(".gitignore", git.concat(common).join("\n"), (): void => writeFile(".npmignore", npm.concat(common).join("\n"), (): void => {})); +if(process.argv[2] === "readme") readFile("README.md", "utf8", readme);