LoggerTest.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Chris Narkiewicz
  5. * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.client.logger
  21. import android.os.Handler
  22. import com.nextcloud.client.core.Clock
  23. import com.nextcloud.client.core.ClockImpl
  24. import com.nhaarman.mockitokotlin2.any
  25. import com.nhaarman.mockitokotlin2.argThat
  26. import com.nhaarman.mockitokotlin2.capture
  27. import com.nhaarman.mockitokotlin2.doAnswer
  28. import com.nhaarman.mockitokotlin2.inOrder
  29. import com.nhaarman.mockitokotlin2.mock
  30. import com.nhaarman.mockitokotlin2.spy
  31. import com.nhaarman.mockitokotlin2.times
  32. import com.nhaarman.mockitokotlin2.verify
  33. import com.nhaarman.mockitokotlin2.whenever
  34. import org.junit.Assert.assertEquals
  35. import org.junit.Assert.assertFalse
  36. import org.junit.Assert.assertNotEquals
  37. import org.junit.Assert.assertNull
  38. import org.junit.Assert.assertTrue
  39. import org.junit.Before
  40. import org.junit.Test
  41. import org.mockito.ArgumentCaptor
  42. import org.mockito.MockitoAnnotations
  43. import java.nio.file.Files
  44. import java.util.concurrent.CountDownLatch
  45. import java.util.concurrent.TimeUnit
  46. class LoggerTest {
  47. private companion object {
  48. const val QUEUE_CAPACITY = 100
  49. const val FILE_SIZE = 1024L
  50. const val LATCH_WAIT = 3L
  51. const val LATCH_INIT = 3
  52. const val EMPTY = 0
  53. const val EMPTY_LONG = 0L
  54. const val TIMEOUT = 3000L
  55. const val MESSAGE_COUNT = 3
  56. }
  57. private lateinit var clock: Clock
  58. private lateinit var logHandler: FileLogHandler
  59. private lateinit var osHandler: Handler
  60. private lateinit var logger: LoggerImpl
  61. @Before
  62. fun setUp() {
  63. MockitoAnnotations.initMocks(this)
  64. val tempDir = Files.createTempDirectory("log-test").toFile()
  65. clock = ClockImpl()
  66. logHandler = spy(FileLogHandler(tempDir, "log.txt", FILE_SIZE))
  67. osHandler = mock()
  68. logger = LoggerImpl(clock, logHandler, osHandler, QUEUE_CAPACITY)
  69. }
  70. @Test
  71. fun `write is done on background thread`() {
  72. val callerThreadId = Thread.currentThread().id
  73. val writerThreadIds = mutableListOf<Long>()
  74. val latch = CountDownLatch(LATCH_INIT)
  75. doAnswer {
  76. writerThreadIds.add(Thread.currentThread().id)
  77. it.callRealMethod()
  78. latch.countDown()
  79. }.whenever(logHandler).open()
  80. doAnswer {
  81. writerThreadIds.add(Thread.currentThread().id)
  82. it.callRealMethod()
  83. latch.countDown()
  84. }.whenever(logHandler).write(any())
  85. doAnswer {
  86. writerThreadIds.add(Thread.currentThread().id)
  87. it.callRealMethod()
  88. latch.countDown()
  89. }.whenever(logHandler).close()
  90. // GIVEN
  91. // logger event loop is running
  92. logger.start()
  93. // WHEN
  94. // message is logged
  95. logger.d("tag", "message")
  96. // THEN
  97. // message is processed on bg thread
  98. // all handler invocations happen on bg thread
  99. // all handler invocations happen on single thread
  100. assertTrue(latch.await(LATCH_WAIT, TimeUnit.SECONDS))
  101. writerThreadIds.forEach { writerThreadId ->
  102. assertNotEquals("All requests must be made on bg thread", callerThreadId, writerThreadId)
  103. }
  104. writerThreadIds.forEach {
  105. assertEquals("All requests must be made on single thread", writerThreadIds[0], it)
  106. }
  107. }
  108. @Test
  109. fun `message is written via log handler`() {
  110. val tag = "test tag"
  111. val message = "test log message"
  112. val latch = CountDownLatch(LATCH_INIT)
  113. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).open()
  114. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).write(any())
  115. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).close()
  116. // GIVEN
  117. // logger event loop is running
  118. logger.start()
  119. // WHEN
  120. // log message is written
  121. logger.d(tag, message)
  122. // THEN
  123. // log handler opens log file
  124. // log handler writes entry
  125. // log handler closes log file
  126. // no lost messages
  127. val called = latch.await(LATCH_WAIT, TimeUnit.SECONDS)
  128. assertTrue("Expected open(), write() and close() calls on bg thread", called)
  129. val inOrder = inOrder(logHandler)
  130. inOrder.verify(logHandler).open()
  131. inOrder.verify(logHandler).write(
  132. argThat {
  133. tag in this && message in this
  134. }
  135. )
  136. inOrder.verify(logHandler).close()
  137. assertFalse(logger.lostEntries)
  138. }
  139. @Test
  140. fun `logs are loaded in background thread and posted to main thread`() {
  141. val currentThreadId = Thread.currentThread().id
  142. var loggerThreadId: Long = -1
  143. val listener: OnLogsLoaded = mock()
  144. val latch = CountDownLatch(2)
  145. // log handler will be called on bg thread
  146. doAnswer {
  147. loggerThreadId = Thread.currentThread().id
  148. latch.countDown()
  149. it.callRealMethod()
  150. }.whenever(logHandler).loadLogFiles(any())
  151. // os handler will be called on bg thread
  152. whenever(osHandler.post(any())).thenAnswer {
  153. latch.countDown()
  154. true
  155. }
  156. // GIVEN
  157. // logger event loop is running
  158. logger.start()
  159. // WHEN
  160. // messages are logged
  161. // log contents are requested
  162. logger.d("tag", "message 1")
  163. logger.d("tag", "message 2")
  164. logger.d("tag", "message 3")
  165. logger.load(listener)
  166. val called = latch.await(LATCH_WAIT, TimeUnit.SECONDS)
  167. assertTrue("Response not posted", called)
  168. // THEN
  169. // log contents are loaded on background thread
  170. // logs are posted to main thread handler
  171. // contents contain logged messages
  172. // messages are in order of writes
  173. assertNotEquals(currentThreadId, loggerThreadId)
  174. val postedCaptor = ArgumentCaptor.forClass(Runnable::class.java)
  175. verify(osHandler).post(capture(postedCaptor))
  176. postedCaptor.value.run()
  177. val logsCaptor = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<LogEntry>>
  178. val sizeCaptor = ArgumentCaptor.forClass(Long::class.java)
  179. verify(listener).invoke(capture(logsCaptor), capture(sizeCaptor))
  180. assertEquals(MESSAGE_COUNT, logsCaptor.value.size)
  181. assertTrue("message 1" in logsCaptor.value[0].message)
  182. assertTrue("message 2" in logsCaptor.value[1].message)
  183. assertTrue("message 3" in logsCaptor.value[2].message)
  184. }
  185. @Test
  186. fun `log level can be decoded from tags`() {
  187. Level.values().forEach {
  188. val decodedLevel = Level.fromTag(it.tag)
  189. assertEquals(it, decodedLevel)
  190. }
  191. }
  192. @Test
  193. fun `queue limit is enforced`() {
  194. // GIVEN
  195. // logger event loop is no running
  196. // WHEN
  197. // queue is filled up to it's capacity
  198. for (i in 0 until QUEUE_CAPACITY + 1) {
  199. logger.d("tag", "Message $i")
  200. }
  201. // THEN
  202. // overflow flag is raised
  203. assertTrue(logger.lostEntries)
  204. }
  205. @Test
  206. fun `queue overflow warning is logged`() {
  207. // GIVEN
  208. // logger loop is overflown
  209. for (i in 0..QUEUE_CAPACITY + 1) {
  210. logger.d("tag", "Message $i")
  211. }
  212. // WHEN
  213. // logger event loop processes events
  214. //
  215. logger.start()
  216. // THEN
  217. // overflow occurrence is logged
  218. val posted = CountDownLatch(1)
  219. whenever(osHandler.post(any())).thenAnswer {
  220. (it.arguments[0] as Runnable).run()
  221. posted.countDown()
  222. true
  223. }
  224. val listener: OnLogsLoaded = mock()
  225. logger.load(listener)
  226. assertTrue("Logs not loaded", posted.await(1, TimeUnit.SECONDS))
  227. verify(listener).invoke(
  228. argThat {
  229. "Logger queue overflow" in last().message
  230. },
  231. any()
  232. )
  233. }
  234. @Test
  235. fun `all log files are deleted`() {
  236. val latch = CountDownLatch(1)
  237. doAnswer {
  238. it.callRealMethod()
  239. latch.countDown()
  240. }.whenever(logHandler).deleteAll()
  241. // GIVEN
  242. // logger is started
  243. logger.start()
  244. // WHEN
  245. // logger has some writes
  246. // logs are deleted
  247. logger.d("tag", "message")
  248. logger.d("tag", "message")
  249. logger.d("tag", "message")
  250. logger.deleteAll()
  251. // THEN
  252. // handler writes files
  253. // handler deletes all files
  254. assertTrue(latch.await(LATCH_WAIT, TimeUnit.SECONDS))
  255. verify(logHandler, times(MESSAGE_COUNT)).write(any())
  256. verify(logHandler).deleteAll()
  257. val loaded = logHandler.loadLogFiles(logHandler.maxLogFilesCount)
  258. assertEquals(EMPTY, loaded.lines.size)
  259. assertEquals(EMPTY_LONG, loaded.logSize)
  260. }
  261. @Test
  262. @Suppress("TooGenericExceptionCaught")
  263. fun `thread interruption is handled while posting log message`() {
  264. Thread {
  265. val callerThread = Thread.currentThread()
  266. // GIVEN
  267. // logger is running
  268. // caller thread is interrupted
  269. logger.start()
  270. callerThread.interrupt()
  271. // WHEN
  272. // message is logged on interrupted thread
  273. var loggerException: Throwable? = null
  274. try {
  275. logger.d("test", "test")
  276. } catch (ex: Throwable) {
  277. loggerException = ex
  278. }
  279. // THEN
  280. // message post is gracefully skipped
  281. // thread interruption flag is not cleared
  282. assertNull(loggerException)
  283. assertTrue("Expected current thread to stay interrupted", callerThread.isInterrupted)
  284. }.apply {
  285. start()
  286. join(TIMEOUT)
  287. }
  288. }
  289. }