Add proper tests, exception handling, implement getAbstractName

This commit is contained in:
John J. Camilleri
2021-10-04 12:10:29 +02:00
parent 314db3ea7f
commit 45db11b669
8 changed files with 3323 additions and 98 deletions

View File

@@ -0,0 +1,49 @@
interface ErrorInfo {
errno: number
code: string
description: string
}
// taken from /usr/include/asm-generic/errno-base.h
const errs: ErrorInfo[] = [
{ code: 'EPERM', errno: 1, description: 'Operation not permitted' },
{ code: 'ENOENT', errno: 2, description: 'No such file or directory' },
{ code: 'ESRCH', errno: 3, description: 'No such process' },
{ code: 'EINTR', errno: 4, description: 'Interrupted system call' },
{ code: 'EIO', errno: 5, description: 'I/O error' },
{ code: 'ENXIO', errno: 6, description: 'No such device or address' },
{ code: 'E2BIG', errno: 7, description: 'Argument list too long' },
{ code: 'ENOEXEC', errno: 8, description: 'Exec format error' },
{ code: 'EBADF', errno: 9, description: 'Bad file number' },
{ code: 'ECHILD', errno: 10, description: 'No child processes' },
{ code: 'EAGAIN', errno: 11, description: 'Try again' },
{ code: 'ENOMEM', errno: 12, description: 'Out of memory' },
{ code: 'EACCES', errno: 13, description: 'Permission denied' },
{ code: 'EFAULT', errno: 14, description: 'Bad address' },
{ code: 'ENOTBLK', errno: 15, description: 'Block device required' },
{ code: 'EBUSY', errno: 16, description: 'Device or resource busy' },
{ code: 'EEXIST', errno: 17, description: 'File exists' },
{ code: 'EXDEV', errno: 18, description: 'Cross-device link' },
{ code: 'ENODEV', errno: 19, description: 'No such device' },
{ code: 'ENOTDIR', errno: 20, description: 'Not a directory' },
{ code: 'EISDIR', errno: 21, description: 'Is a directory' },
{ code: 'EINVAL', errno: 22, description: 'Invalid argument' },
{ code: 'ENFILE', errno: 23, description: 'File table overflow' },
{ code: 'EMFILE', errno: 24, description: 'Too many open files' },
{ code: 'ENOTTY', errno: 25, description: 'Not a typewriter' },
{ code: 'ETXTBSY', errno: 26, description: 'Text file busy' },
{ code: 'EFBIG', errno: 27, description: 'File too large' },
{ code: 'ENOSPC', errno: 28, description: 'No space left on device' },
{ code: 'ESPIPE', errno: 29, description: 'Illegal seek' },
{ code: 'EROFS', errno: 30, description: 'Read-only file system' },
{ code: 'EMLINK', errno: 31, description: 'Too many links' },
{ code: 'EPIPE', errno: 32, description: 'Broken pipe' },
{ code: 'EDOM', errno: 33, description: 'Math argument out of domain of func' },
{ code: 'ERANGE', errno: 34, description: 'Math result not representable' }
]
export default {
lookup(errno: number): string | undefined {
return errs.find(err => err.errno === errno)?.description
}
}

View File

@@ -1,44 +1,122 @@
import errno from './errno'
import ffi from 'ffi'
import { deref } from 'ref'
import * as ref from 'ref'
import * as ffi from 'ffi'
import ref from 'ref'
import Struct from 'ref-struct'
// ----------------------------------------------------------------------------
// FFI "Types"
const PgfDB = ref.types.int
const PgfDBPtr = ref.refType(PgfDB)
const PgfRevision = ref.types.int
const PgfRevisionPtr = ref.refType(PgfRevision)
const PgfExn = ref.types.int
const PgfExnPtr = ref.refType(PgfExn)
// const stringPtr = ref.refType(ref.types.CString)
const PGF = ffi.Library('libpgf', {
'pgf_read_pgf': [ PgfDBPtr, [ 'string', PgfRevisionPtr, PgfExnPtr ] ],
'pgf_write_pgf': [ 'void', [ 'string', PgfDBPtr, PgfRevisionPtr, PgfExnPtr ] ],
const PgfExn = Struct({
type: ref.types.int,
code: ref.types.int,
msg: ref.types.CString
})
const PgfExnPtr = ref.refType(PgfExn)
const PgfText = Struct({
size: ref.types.size_t,
text: ref.types.char // char[]
})
const PgfTextPtr = ref.refType(PgfText)
// ----------------------------------------------------------------------------
// TypeScript Types
interface Pointer extends Buffer {
deref(): any
}
interface State {
db: Pointer,
revision: number
}
const state: State = {
db: ref.NULL_POINTER as Pointer,
revision: 0
}
export default {
readPGF: function (path: string) {
const rev = ref.alloc(PgfRevision) as Pointer
const err = ref.alloc(PgfExn) as Pointer
const db = PGF.pgf_read_pgf(path, rev, err)
if (err.deref() === 0) {
state.db = db
state.revision = rev.deref()
console.log(`ok`)
} else {
console.error(`error ${err.deref()}`)
}
class PGFError extends Error {
constructor(message: string) {
super(message)
}
}
// ----------------------------------------------------------------------------
// FFI
const runtime = ffi.Library('libpgf', {
'pgf_read_pgf': [ PgfDBPtr, [ ref.types.CString, PgfRevisionPtr, PgfExnPtr ] ],
'pgf_write_pgf': [ ref.types.void, [ ref.types.CString, PgfDBPtr, PgfRevisionPtr, PgfExnPtr ] ],
'pgf_abstract_name': [ PgfTextPtr, [ PgfDBPtr, PgfRevision, PgfExnPtr ] ],
})
// ----------------------------------------------------------------------------
// Helpers
function handleError (errPtr: Pointer): void {
const err = errPtr.deref()
switch (err.type) {
// PGF_EXN_NONE
case 0: return
// PGF_EXN_SYSTEM_ERROR
case 1:
const desc = errno.lookup(err.code)
throw new Error(`${desc}: ${err.msg}`)
// PGF_EXN_PGF_ERROR
case 2:
throw new PGFError(err.msg)
// PGF_EXN_OTHER_ERROR
case 3:
throw new Error(err.msg)
default:
throw new Error(`unknown error type: ${err.type}`)
}
}
function PgfText_AsString (txtPtr: Pointer) {
const txtSize = txtPtr.deref().size
const charPtr = ref.reinterpret(txtPtr, txtSize, ref.types.size_t.size)
return charPtr.toString('utf8')
}
// ----------------------------------------------------------------------------
// PGF grammar object
class PGF {
db: Pointer
revision: number
constructor () {
this.db = ref.NULL_POINTER as Pointer
this.revision = 0
}
getAbstractName () {
const err = ref.alloc(PgfExn) as Pointer
const txt = runtime.pgf_abstract_name(this.db, this.revision, err)
handleError(err)
return PgfText_AsString(txt)
}
}
// ----------------------------------------------------------------------------
// PGF module functions
function readPGF (path: string): PGF {
const rev = ref.alloc(PgfRevision) as Pointer
const err = ref.alloc(PgfExn) as Pointer
const db = runtime.pgf_read_pgf(path, rev, err)
handleError(err)
const state = new PGF()
state.db = db
state.revision = rev.deref()
return state
}
// ----------------------------------------------------------------------------
// Exposed library API
export default {
readPGF
}

View File

@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "dist/index.js",
"scripts": {
"build": "npx tsc",
"test": "ts-node ./tests/basic.ts"
"test": "npx jest"
},
"author": "John J. Camilleri",
"license": "ISC",
@@ -14,7 +14,9 @@
},
"devDependencies": {
"@types/ffi": "^0.2.3",
"ts-node": "^10.2.1",
"@types/jest": "^27.0.2",
"jest": "^27.2.4",
"ts-jest": "^27.0.5",
"typescript": "^4.4.3"
}
}

View File

@@ -0,0 +1,16 @@
import PGF from '../index'
test('readPGF successful', () => {
let gr = PGF.readPGF('../haskell/tests/basic.pgf')
})
test('readPGF missing file', () => {
expect(() => {
PGF.readPGF('abc.pgf')
}).toThrow()
})
test('abstract name', () => {
let gr = PGF.readPGF('../haskell/tests/basic.pgf')
expect(gr.getAbstractName()).toBe('basic')
})

View File

@@ -1,3 +0,0 @@
import PGF from '../index'
PGF.readPGF('../haskell/tests/basic.pgf')