Fixture.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { Api } from './Api'
  2. import { assert } from 'chai'
  3. import { ISubmittableResult } from '@polkadot/types/types/'
  4. import { DispatchResult } from '@polkadot/types/interfaces/system'
  5. import { QueryNodeApi } from './QueryNodeApi'
  6. import { SubmittableExtrinsic } from '@polkadot/api/types'
  7. import { extendDebug, Debugger } from './Debugger'
  8. import { AnyQueryNodeEvent, EventDetails } from './types'
  9. export abstract class BaseFixture {
  10. protected readonly api: Api
  11. protected debug: Debugger.Debugger
  12. private _executed = false
  13. // The reason of the "Unexpected" failure of running the fixture
  14. private _err: Error | undefined = undefined
  15. constructor(api: Api) {
  16. this.api = api
  17. this.debug = extendDebug(`fixture:${this.constructor.name}`)
  18. }
  19. // Derviced classes must not override this
  20. public async runner(): Promise<void> {
  21. await this.execute()
  22. this._executed = true
  23. }
  24. abstract execute(): Promise<void>
  25. // Used by execution implementation to signal failure
  26. protected error(err: Error): void {
  27. this._err = err
  28. }
  29. get executed(): boolean {
  30. return this._executed
  31. }
  32. public didFail(): boolean {
  33. if (!this.executed) {
  34. throw new Error('Trying to check execution result before running fixture')
  35. }
  36. return this._err !== undefined
  37. }
  38. public executionError(): Error | undefined {
  39. if (!this.executed) {
  40. throw new Error('Trying to check execution result before running fixture')
  41. }
  42. return this._err
  43. }
  44. protected expectDispatchError(result: ISubmittableResult, errMessage: string): ISubmittableResult {
  45. const success = result.findRecord('system', 'ExtrinsicSuccess')
  46. if (success) {
  47. const sudid = result.findRecord('sudo', 'Sudid')
  48. if (sudid) {
  49. const dispatchResult = sudid.event.data[0] as DispatchResult
  50. if (dispatchResult.isOk) {
  51. this.error(new Error(errMessage))
  52. }
  53. } else {
  54. this.error(new Error(errMessage))
  55. }
  56. }
  57. return result
  58. }
  59. protected expectDispatchSuccess(result: ISubmittableResult, errMessage: string): ISubmittableResult {
  60. const success = result.findRecord('system', 'ExtrinsicSuccess')
  61. if (success) {
  62. const sudid = result.findRecord('sudo', 'Sudid')
  63. if (sudid) {
  64. const dispatchResult = sudid.event.data[0] as DispatchResult
  65. if (dispatchResult.isError) {
  66. this.error(new Error(errMessage))
  67. // Log DispatchError details
  68. }
  69. }
  70. } else {
  71. this.error(new Error(errMessage))
  72. // Log DispatchError
  73. }
  74. return result
  75. }
  76. }
  77. export abstract class BaseQueryNodeFixture extends BaseFixture {
  78. protected readonly query: QueryNodeApi
  79. public readonly queryNodeChecksEnabled: boolean
  80. constructor(api: Api, query: QueryNodeApi) {
  81. super(api)
  82. this.query = query
  83. this.queryNodeChecksEnabled = !process.env.SKIP_QUERY_NODE_CHECKS
  84. }
  85. public async runQueryNodeChecks(): Promise<void> {
  86. if (!this.executed) {
  87. throw new Error('Cannot run query node checks before Fixture is executed')
  88. }
  89. // Implement in child class!
  90. }
  91. protected findMatchingQueryNodeEvent<T extends AnyQueryNodeEvent>(
  92. eventToFind: EventDetails,
  93. queryNodeEvents: T[]
  94. ): T {
  95. const { blockNumber, indexInBlock } = eventToFind
  96. const qEvent = queryNodeEvents.find((e) => e.inBlock === blockNumber && e.indexInBlock === indexInBlock)
  97. if (!qEvent) {
  98. throw new Error(`Could not find matching query-node event (expected ${blockNumber}:${indexInBlock})!`)
  99. }
  100. return qEvent
  101. }
  102. }
  103. export abstract class StandardizedFixture extends BaseQueryNodeFixture {
  104. protected extrinsics: SubmittableExtrinsic<'promise'>[] = []
  105. protected results: ISubmittableResult[] = []
  106. protected events: EventDetails[] = []
  107. protected abstract getSignerAccountOrAccounts(): Promise<string | string[]>
  108. protected abstract getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[] | SubmittableExtrinsic<'promise'>[][]>
  109. protected abstract getEventFromResult(result: ISubmittableResult): Promise<EventDetails>
  110. protected abstract assertQueryNodeEventIsValid(qEvent: AnyQueryNodeEvent, i: number): void
  111. protected assertQueryNodeEventsAreValid(qEvents: AnyQueryNodeEvent[]): void {
  112. this.events.forEach((e, i) => {
  113. const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
  114. assert.equal(qEvent.inExtrinsic, this.extrinsics[i].hash.toString())
  115. assert.equal(new Date(qEvent.createdAt).getTime(), e.blockTimestamp)
  116. this.assertQueryNodeEventIsValid(qEvent, i)
  117. })
  118. }
  119. private flattenExtrinsics(
  120. extrinsics: SubmittableExtrinsic<'promise'>[] | SubmittableExtrinsic<'promise'>[][]
  121. ): SubmittableExtrinsic<'promise'>[] {
  122. return Array.isArray(extrinsics[0])
  123. ? (extrinsics as SubmittableExtrinsic<'promise'>[][]).reduce((res, batch) => res.concat(batch), [])
  124. : (extrinsics as SubmittableExtrinsic<'promise'>[])
  125. }
  126. public async execute(): Promise<void> {
  127. const accountOrAccounts = await this.getSignerAccountOrAccounts()
  128. const extrinsics = await this.getExtrinsics()
  129. this.extrinsics = this.flattenExtrinsics(extrinsics)
  130. await this.api.prepareAccountsForFeeExpenses(accountOrAccounts, this.extrinsics)
  131. this.results = await this.api.sendExtrinsicsAndGetResults(extrinsics, accountOrAccounts)
  132. this.events = await Promise.all(this.results.map((r) => this.getEventFromResult(r)))
  133. }
  134. }
  135. // Runs a fixture and measures how long it took to run
  136. // Ensures fixture only runs once, and asserts that it doesn't fail
  137. export class FixtureRunner {
  138. private fixture: BaseFixture
  139. private ran = false
  140. private queryNodeChecksRan = false
  141. constructor(fixture: BaseFixture) {
  142. this.fixture = fixture
  143. }
  144. public async run(): Promise<void> {
  145. if (this.ran) {
  146. throw new Error('Fixture already ran')
  147. }
  148. this.ran = true
  149. // TODO: record starting block
  150. await this.fixture.runner()
  151. // TODO: record ending block
  152. const err = this.fixture.executionError()
  153. assert.equal(err, undefined)
  154. }
  155. public async runQueryNodeChecks(): Promise<void> {
  156. if (process.env.SKIP_QUERY_NODE_CHECKS) {
  157. return
  158. }
  159. if (!(this.fixture instanceof BaseQueryNodeFixture)) {
  160. throw new Error('Tried to run query node checks for non-query-node fixture!')
  161. }
  162. if (this.queryNodeChecksRan) {
  163. throw new Error('Fixture query node checks already ran')
  164. }
  165. this.queryNodeChecksRan = true
  166. await this.fixture.runQueryNodeChecks()
  167. }
  168. public async runWithQueryNodeChecks(): Promise<void> {
  169. await this.run()
  170. await this.runQueryNodeChecks()
  171. }
  172. }