@vitest/utils
Advanced tools
Comparing version 2.1.3 to 2.1.4
@@ -0,3 +1,3 @@ | ||
import { format as format$1, plugins } from '@vitest/pretty-format'; | ||
import * as loupe from 'loupe'; | ||
import { format as format$1, plugins } from '@vitest/pretty-format'; | ||
@@ -4,0 +4,0 @@ const { |
@@ -45,3 +45,11 @@ import { Nullable, Arrayable } from './types.js'; | ||
declare function isNegativeNaN(val: number): boolean; | ||
/** | ||
* Deep merge :P | ||
* | ||
* Will merge objects only if they are plain | ||
* | ||
* Do not merge types - it is very expensive and usually it's better to case a type here | ||
*/ | ||
declare function deepMerge<T extends object = object>(target: T, ...sources: any[]): T; | ||
export { type DeferPromise, assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray }; | ||
export { type DeferPromise, assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, deepMerge, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray }; |
@@ -191,3 +191,35 @@ function createSimpleStackTrace(options) { | ||
} | ||
function toString(v) { | ||
return Object.prototype.toString.call(v); | ||
} | ||
function isPlainObject(val) { | ||
return toString(val) === "[object Object]" && (!val.constructor || val.constructor.name === "Object"); | ||
} | ||
function isMergeableObject(item) { | ||
return isPlainObject(item) && !Array.isArray(item); | ||
} | ||
function deepMerge(target, ...sources) { | ||
if (!sources.length) { | ||
return target; | ||
} | ||
const source = sources.shift(); | ||
if (source === void 0) { | ||
return target; | ||
} | ||
if (isMergeableObject(target) && isMergeableObject(source)) { | ||
Object.keys(source).forEach((key) => { | ||
const _source = source; | ||
if (isMergeableObject(_source[key])) { | ||
if (!target[key]) { | ||
target[key] = {}; | ||
} | ||
deepMerge(target[key], _source[key]); | ||
} else { | ||
target[key] = _source[key]; | ||
} | ||
}); | ||
} | ||
return deepMerge(target, ...sources); | ||
} | ||
export { assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray }; | ||
export { assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, deepMerge, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray }; |
@@ -1,20 +0,6 @@ | ||
export { DeferPromise, assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray } from './helpers.js'; | ||
import { PrettyFormatOptions } from '@vitest/pretty-format'; | ||
export { DeferPromise, assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, deepMerge, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray } from './helpers.js'; | ||
import { Colors } from 'tinyrainbow'; | ||
export { ArgumentsType, Arrayable, Awaitable, Constructable, DeepMerge, ErrorWithDiff, MergeInsertions, MutableArray, Nullable, ParsedStack, SerializedError, TestError } from './types.js'; | ||
interface SafeTimers { | ||
nextTick: (cb: () => void) => void; | ||
setTimeout: typeof setTimeout; | ||
setInterval: typeof setInterval; | ||
clearInterval: typeof clearInterval; | ||
clearTimeout: typeof clearTimeout; | ||
setImmediate: typeof setImmediate; | ||
clearImmediate: typeof clearImmediate; | ||
} | ||
declare function getSafeTimers(): SafeTimers; | ||
declare function setSafeTimers(): void; | ||
declare function shuffle<T>(array: T[], seed?: number): T[]; | ||
type Inspect = (value: unknown, options: Options) => string; | ||
@@ -43,6 +29,2 @@ interface Options { | ||
declare const lineSplitRE: RegExp; | ||
declare function positionToOffset(source: string, lineNumber: number, columnNumber: number): number; | ||
declare function offsetToLineNumber(source: string, offset: number): number; | ||
interface HighlightOptions { | ||
@@ -56,2 +38,20 @@ jsx?: boolean; | ||
declare const lineSplitRE: RegExp; | ||
declare function positionToOffset(source: string, lineNumber: number, columnNumber: number): number; | ||
declare function offsetToLineNumber(source: string, offset: number): number; | ||
declare function shuffle<T>(array: T[], seed?: number): T[]; | ||
interface SafeTimers { | ||
nextTick: (cb: () => void) => void; | ||
setTimeout: typeof setTimeout; | ||
setInterval: typeof setInterval; | ||
clearInterval: typeof clearInterval; | ||
clearTimeout: typeof clearTimeout; | ||
setImmediate: typeof setImmediate; | ||
clearImmediate: typeof clearImmediate; | ||
} | ||
declare function getSafeTimers(): SafeTimers; | ||
declare function setSafeTimers(): void; | ||
export { type SafeTimers, type StringifyOptions, format, getSafeTimers, highlight, inspect, lineSplitRE, nanoid, objDisplay, offsetToLineNumber, positionToOffset, setSafeTimers, shuffle, stringify }; |
@@ -1,103 +0,8 @@ | ||
export { assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray } from './helpers.js'; | ||
import { g as getDefaultExportFromCjs } from './chunk-_commonjsHelpers.js'; | ||
export { f as format, i as inspect, o as objDisplay, s as stringify } from './chunk-_commonjsHelpers.js'; | ||
export { assertTypes, clone, createDefer, createSimpleStackTrace, deepClone, deepMerge, getCallLastIndex, getOwnProperties, getType, isNegativeNaN, isObject, isPrimitive, noop, notNullish, objectAttr, parseRegexp, slash, toArray } from './helpers.js'; | ||
import c from 'tinyrainbow'; | ||
import '@vitest/pretty-format'; | ||
import 'loupe'; | ||
import '@vitest/pretty-format'; | ||
const SAFE_TIMERS_SYMBOL = Symbol("vitest:SAFE_TIMERS"); | ||
function getSafeTimers() { | ||
const { | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
} = globalThis[SAFE_TIMERS_SYMBOL] || globalThis; | ||
const { nextTick: safeNextTick } = globalThis[SAFE_TIMERS_SYMBOL] || globalThis.process || { nextTick: (cb) => cb() }; | ||
return { | ||
nextTick: safeNextTick, | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
}; | ||
} | ||
function setSafeTimers() { | ||
const { | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
} = globalThis; | ||
const { nextTick: safeNextTick } = globalThis.process || { | ||
nextTick: (cb) => cb() | ||
}; | ||
const timers = { | ||
nextTick: safeNextTick, | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
}; | ||
globalThis[SAFE_TIMERS_SYMBOL] = timers; | ||
} | ||
const RealDate = Date; | ||
function random(seed) { | ||
const x = Math.sin(seed++) * 1e4; | ||
return x - Math.floor(x); | ||
} | ||
function shuffle(array, seed = RealDate.now()) { | ||
let length = array.length; | ||
while (length) { | ||
const index = Math.floor(random(seed) * length--); | ||
const previous = array[length]; | ||
array[length] = array[index]; | ||
array[index] = previous; | ||
++seed; | ||
} | ||
return array; | ||
} | ||
const lineSplitRE = /\r?\n/; | ||
function positionToOffset(source, lineNumber, columnNumber) { | ||
const lines = source.split(lineSplitRE); | ||
const nl = /\r\n/.test(source) ? 2 : 1; | ||
let start = 0; | ||
if (lineNumber > lines.length) { | ||
return source.length; | ||
} | ||
for (let i = 0; i < lineNumber - 1; i++) { | ||
start += lines[i].length + nl; | ||
} | ||
return start + columnNumber; | ||
} | ||
function offsetToLineNumber(source, offset) { | ||
if (offset > source.length) { | ||
throw new Error( | ||
`offset is longer than source length! offset ${offset} > length ${source.length}` | ||
); | ||
} | ||
const lines = source.split(lineSplitRE); | ||
const nl = /\r\n/.test(source) ? 2 : 1; | ||
let counted = 0; | ||
let line = 0; | ||
for (; line < lines.length; line++) { | ||
const lineLength = lines[line].length + nl; | ||
if (counted + lineLength >= offset) { | ||
break; | ||
} | ||
counted += lineLength; | ||
} | ||
return line + 1; | ||
} | ||
var jsTokens_1; | ||
@@ -647,2 +552,97 @@ var hasRequiredJsTokens; | ||
const lineSplitRE = /\r?\n/; | ||
function positionToOffset(source, lineNumber, columnNumber) { | ||
const lines = source.split(lineSplitRE); | ||
const nl = /\r\n/.test(source) ? 2 : 1; | ||
let start = 0; | ||
if (lineNumber > lines.length) { | ||
return source.length; | ||
} | ||
for (let i = 0; i < lineNumber - 1; i++) { | ||
start += lines[i].length + nl; | ||
} | ||
return start + columnNumber; | ||
} | ||
function offsetToLineNumber(source, offset) { | ||
if (offset > source.length) { | ||
throw new Error( | ||
`offset is longer than source length! offset ${offset} > length ${source.length}` | ||
); | ||
} | ||
const lines = source.split(lineSplitRE); | ||
const nl = /\r\n/.test(source) ? 2 : 1; | ||
let counted = 0; | ||
let line = 0; | ||
for (; line < lines.length; line++) { | ||
const lineLength = lines[line].length + nl; | ||
if (counted + lineLength >= offset) { | ||
break; | ||
} | ||
counted += lineLength; | ||
} | ||
return line + 1; | ||
} | ||
const RealDate = Date; | ||
function random(seed) { | ||
const x = Math.sin(seed++) * 1e4; | ||
return x - Math.floor(x); | ||
} | ||
function shuffle(array, seed = RealDate.now()) { | ||
let length = array.length; | ||
while (length) { | ||
const index = Math.floor(random(seed) * length--); | ||
const previous = array[length]; | ||
array[length] = array[index]; | ||
array[index] = previous; | ||
++seed; | ||
} | ||
return array; | ||
} | ||
const SAFE_TIMERS_SYMBOL = Symbol("vitest:SAFE_TIMERS"); | ||
function getSafeTimers() { | ||
const { | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
} = globalThis[SAFE_TIMERS_SYMBOL] || globalThis; | ||
const { nextTick: safeNextTick } = globalThis[SAFE_TIMERS_SYMBOL] || globalThis.process || { nextTick: (cb) => cb() }; | ||
return { | ||
nextTick: safeNextTick, | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
}; | ||
} | ||
function setSafeTimers() { | ||
const { | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
} = globalThis; | ||
const { nextTick: safeNextTick } = globalThis.process || { | ||
nextTick: (cb) => cb() | ||
}; | ||
const timers = { | ||
nextTick: safeNextTick, | ||
setTimeout: safeSetTimeout, | ||
setInterval: safeSetInterval, | ||
clearInterval: safeClearInterval, | ||
clearTimeout: safeClearTimeout, | ||
setImmediate: safeSetImmediate, | ||
clearImmediate: safeClearImmediate | ||
}; | ||
globalThis[SAFE_TIMERS_SYMBOL] = timers; | ||
} | ||
export { getSafeTimers, highlight, lineSplitRE, nanoid, offsetToLineNumber, positionToOffset, setSafeTimers, shuffle }; |
import { notNullish, isPrimitive } from './helpers.js'; | ||
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//; | ||
function normalizeWindowsPath(input = "") { | ||
if (!input) { | ||
return input; | ||
} | ||
return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase()); | ||
} | ||
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/; | ||
function cwd() { | ||
if (typeof process !== "undefined" && typeof process.cwd === "function") { | ||
return process.cwd().replace(/\\/g, "/"); | ||
} | ||
return "/"; | ||
} | ||
const resolve$2 = function(...arguments_) { | ||
arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument)); | ||
let resolvedPath = ""; | ||
let resolvedAbsolute = false; | ||
for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) { | ||
const path = index >= 0 ? arguments_[index] : cwd(); | ||
if (!path || path.length === 0) { | ||
continue; | ||
} | ||
resolvedPath = `${path}/${resolvedPath}`; | ||
resolvedAbsolute = isAbsolute(path); | ||
} | ||
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); | ||
if (resolvedAbsolute && !isAbsolute(resolvedPath)) { | ||
return `/${resolvedPath}`; | ||
} | ||
return resolvedPath.length > 0 ? resolvedPath : "."; | ||
}; | ||
function normalizeString(path, allowAboveRoot) { | ||
let res = ""; | ||
let lastSegmentLength = 0; | ||
let lastSlash = -1; | ||
let dots = 0; | ||
let char = null; | ||
for (let index = 0; index <= path.length; ++index) { | ||
if (index < path.length) { | ||
char = path[index]; | ||
} else if (char === "/") { | ||
break; | ||
} else { | ||
char = "/"; | ||
} | ||
if (char === "/") { | ||
if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) { | ||
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") { | ||
if (res.length > 2) { | ||
const lastSlashIndex = res.lastIndexOf("/"); | ||
if (lastSlashIndex === -1) { | ||
res = ""; | ||
lastSegmentLength = 0; | ||
} else { | ||
res = res.slice(0, lastSlashIndex); | ||
lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); | ||
} | ||
lastSlash = index; | ||
dots = 0; | ||
continue; | ||
} else if (res.length > 0) { | ||
res = ""; | ||
lastSegmentLength = 0; | ||
lastSlash = index; | ||
dots = 0; | ||
continue; | ||
} | ||
} | ||
if (allowAboveRoot) { | ||
res += res.length > 0 ? "/.." : ".."; | ||
lastSegmentLength = 2; | ||
} | ||
} else { | ||
if (res.length > 0) { | ||
res += `/${path.slice(lastSlash + 1, index)}`; | ||
} else { | ||
res = path.slice(lastSlash + 1, index); | ||
} | ||
lastSegmentLength = index - lastSlash - 1; | ||
} | ||
lastSlash = index; | ||
dots = 0; | ||
} else if (char === "." && dots !== -1) { | ||
++dots; | ||
} else { | ||
dots = -1; | ||
} | ||
} | ||
return res; | ||
} | ||
const isAbsolute = function(p) { | ||
return _IS_ABSOLUTE_RE.test(p); | ||
}; | ||
const comma = ','.charCodeAt(0); | ||
@@ -380,3 +285,3 @@ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; | ||
*/ | ||
function resolve$1(input, base) { | ||
function resolve$2(input, base) { | ||
if (!input && !base) | ||
@@ -441,3 +346,3 @@ return ''; | ||
function resolve(input, base) { | ||
function resolve$1(input, base) { | ||
// The base is always treated as a directory, if it's not empty. | ||
@@ -448,3 +353,3 @@ // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327 | ||
base += '/'; | ||
return resolve$1(input, base); | ||
return resolve$2(input, base); | ||
} | ||
@@ -649,4 +554,4 @@ | ||
this.ignoreList = parsed.ignoreList || parsed.x_google_ignoreList || undefined; | ||
const from = resolve(sourceRoot || '', stripFilename(mapUrl)); | ||
this.resolvedSources = sources.map((s) => resolve(s || '', from)); | ||
const from = resolve$1(sourceRoot || '', stripFilename(mapUrl)); | ||
this.resolvedSources = sources.map((s) => resolve$1(s || '', from)); | ||
const { mappings } = parsed; | ||
@@ -785,2 +690,97 @@ if (typeof mappings === 'string') { | ||
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//; | ||
function normalizeWindowsPath(input = "") { | ||
if (!input) { | ||
return input; | ||
} | ||
return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase()); | ||
} | ||
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/; | ||
function cwd() { | ||
if (typeof process !== "undefined" && typeof process.cwd === "function") { | ||
return process.cwd().replace(/\\/g, "/"); | ||
} | ||
return "/"; | ||
} | ||
const resolve = function(...arguments_) { | ||
arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument)); | ||
let resolvedPath = ""; | ||
let resolvedAbsolute = false; | ||
for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) { | ||
const path = index >= 0 ? arguments_[index] : cwd(); | ||
if (!path || path.length === 0) { | ||
continue; | ||
} | ||
resolvedPath = `${path}/${resolvedPath}`; | ||
resolvedAbsolute = isAbsolute(path); | ||
} | ||
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); | ||
if (resolvedAbsolute && !isAbsolute(resolvedPath)) { | ||
return `/${resolvedPath}`; | ||
} | ||
return resolvedPath.length > 0 ? resolvedPath : "."; | ||
}; | ||
function normalizeString(path, allowAboveRoot) { | ||
let res = ""; | ||
let lastSegmentLength = 0; | ||
let lastSlash = -1; | ||
let dots = 0; | ||
let char = null; | ||
for (let index = 0; index <= path.length; ++index) { | ||
if (index < path.length) { | ||
char = path[index]; | ||
} else if (char === "/") { | ||
break; | ||
} else { | ||
char = "/"; | ||
} | ||
if (char === "/") { | ||
if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) { | ||
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") { | ||
if (res.length > 2) { | ||
const lastSlashIndex = res.lastIndexOf("/"); | ||
if (lastSlashIndex === -1) { | ||
res = ""; | ||
lastSegmentLength = 0; | ||
} else { | ||
res = res.slice(0, lastSlashIndex); | ||
lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); | ||
} | ||
lastSlash = index; | ||
dots = 0; | ||
continue; | ||
} else if (res.length > 0) { | ||
res = ""; | ||
lastSegmentLength = 0; | ||
lastSlash = index; | ||
dots = 0; | ||
continue; | ||
} | ||
} | ||
if (allowAboveRoot) { | ||
res += res.length > 0 ? "/.." : ".."; | ||
lastSegmentLength = 2; | ||
} | ||
} else { | ||
if (res.length > 0) { | ||
res += `/${path.slice(lastSlash + 1, index)}`; | ||
} else { | ||
res = path.slice(lastSlash + 1, index); | ||
} | ||
lastSegmentLength = index - lastSlash - 1; | ||
} | ||
lastSlash = index; | ||
dots = 0; | ||
} else if (char === "." && dots !== -1) { | ||
++dots; | ||
} else { | ||
dots = -1; | ||
} | ||
} | ||
return res; | ||
} | ||
const isAbsolute = function(p) { | ||
return _IS_ABSOLUTE_RE.test(p); | ||
}; | ||
const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m; | ||
@@ -894,3 +894,3 @@ const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/; | ||
} | ||
file = resolve$2(file); | ||
file = resolve(file); | ||
if (method) { | ||
@@ -897,0 +897,0 @@ method = method.replace(/__vite_ssr_import_\d+__\./g, ""); |
{ | ||
"name": "@vitest/utils", | ||
"type": "module", | ||
"version": "2.1.3", | ||
"version": "2.1.4", | ||
"description": "Shared Vitest utility functions", | ||
@@ -63,9 +63,9 @@ "license": "MIT", | ||
"dependencies": { | ||
"loupe": "^3.1.1", | ||
"loupe": "^3.1.2", | ||
"tinyrainbow": "^1.2.0", | ||
"@vitest/pretty-format": "2.1.3" | ||
"@vitest/pretty-format": "2.1.4" | ||
}, | ||
"devDependencies": { | ||
"@jridgewell/trace-mapping": "^0.3.25", | ||
"@types/estree": "^1.0.5", | ||
"@types/estree": "^1.0.6", | ||
"diff-sequences": "^29.6.3", | ||
@@ -72,0 +72,0 @@ "tinyhighlight": "^0.3.2" |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
156585
4472
+ Added@vitest/pretty-format@2.1.4(transitive)
- Removed@vitest/pretty-format@2.1.3(transitive)
Updated@vitest/pretty-format@2.1.4
Updatedloupe@^3.1.2