LoggerTest.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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 org.junit.Assert.assertEquals
  25. import org.junit.Assert.assertFalse
  26. import org.junit.Assert.assertNotEquals
  27. import org.junit.Assert.assertNotNull
  28. import org.junit.Assert.assertNull
  29. import org.junit.Assert.assertTrue
  30. import org.junit.Before
  31. import org.junit.Test
  32. import org.mockito.ArgumentCaptor
  33. import org.mockito.MockitoAnnotations
  34. import org.mockito.kotlin.any
  35. import org.mockito.kotlin.argThat
  36. import org.mockito.kotlin.capture
  37. import org.mockito.kotlin.doAnswer
  38. import org.mockito.kotlin.inOrder
  39. import org.mockito.kotlin.mock
  40. import org.mockito.kotlin.spy
  41. import org.mockito.kotlin.times
  42. import org.mockito.kotlin.verify
  43. import org.mockito.kotlin.whenever
  44. import java.nio.file.Files
  45. import java.util.concurrent.CountDownLatch
  46. import java.util.concurrent.TimeUnit
  47. class LoggerTest {
  48. private companion object {
  49. const val QUEUE_CAPACITY = 100
  50. const val FILE_SIZE = 1024L
  51. const val LATCH_WAIT = 3L
  52. const val LATCH_INIT = 3
  53. const val EMPTY = 0
  54. const val EMPTY_LONG = 0L
  55. const val TIMEOUT = 3000L
  56. const val MESSAGE_COUNT = 3
  57. }
  58. private lateinit var clock: Clock
  59. private lateinit var logHandler: FileLogHandler
  60. private lateinit var osHandler: Handler
  61. private lateinit var logger: LoggerImpl
  62. @Before
  63. fun setUp() {
  64. MockitoAnnotations.initMocks(this)
  65. val tempDir = Files.createTempDirectory("log-test").toFile()
  66. clock = ClockImpl()
  67. logHandler = spy(FileLogHandler(tempDir, "log.txt", FILE_SIZE))
  68. osHandler = mock()
  69. logger = LoggerImpl(clock, logHandler, osHandler, QUEUE_CAPACITY)
  70. }
  71. @Test
  72. fun `write is done on background thread`() {
  73. val callerThreadId = Thread.currentThread().id
  74. val writerThreadIds = mutableListOf<Long>()
  75. val latch = CountDownLatch(LATCH_INIT)
  76. doAnswer {
  77. writerThreadIds.add(Thread.currentThread().id)
  78. it.callRealMethod()
  79. latch.countDown()
  80. }.whenever(logHandler).open()
  81. doAnswer {
  82. writerThreadIds.add(Thread.currentThread().id)
  83. it.callRealMethod()
  84. latch.countDown()
  85. }.whenever(logHandler).write(any())
  86. doAnswer {
  87. writerThreadIds.add(Thread.currentThread().id)
  88. it.callRealMethod()
  89. latch.countDown()
  90. }.whenever(logHandler).close()
  91. // GIVEN
  92. // logger event loop is running
  93. logger.start()
  94. // WHEN
  95. // message is logged
  96. logger.d("tag", "message")
  97. // THEN
  98. // message is processed on bg thread
  99. // all handler invocations happen on bg thread
  100. // all handler invocations happen on single thread
  101. assertTrue(latch.await(LATCH_WAIT, TimeUnit.SECONDS))
  102. writerThreadIds.forEach { writerThreadId ->
  103. assertNotEquals("All requests must be made on bg thread", callerThreadId, writerThreadId)
  104. }
  105. writerThreadIds.forEach {
  106. assertEquals("All requests must be made on single thread", writerThreadIds[0], it)
  107. }
  108. }
  109. @Test
  110. fun `message is written via log handler`() {
  111. val tag = "test tag"
  112. val message = "test log message"
  113. val latch = CountDownLatch(LATCH_INIT)
  114. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).open()
  115. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).write(any())
  116. doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).close()
  117. // GIVEN
  118. // logger event loop is running
  119. logger.start()
  120. // WHEN
  121. // log message is written
  122. logger.d(tag, message)
  123. // THEN
  124. // log handler opens log file
  125. // log handler writes entry
  126. // log handler closes log file
  127. // no lost messages
  128. val called = latch.await(LATCH_WAIT, TimeUnit.SECONDS)
  129. assertTrue("Expected open(), write() and close() calls on bg thread", called)
  130. val inOrder = inOrder(logHandler)
  131. inOrder.verify(logHandler).open()
  132. inOrder.verify(logHandler).write(
  133. argThat {
  134. tag in this && message in this
  135. }
  136. )
  137. inOrder.verify(logHandler).close()
  138. assertFalse(logger.lostEntries)
  139. }
  140. @Test
  141. fun `logs are loaded in background thread and posted to main thread`() {
  142. val currentThreadId = Thread.currentThread().id
  143. var loggerThreadId: Long = -1
  144. val listener: OnLogsLoaded = mock()
  145. val latch = CountDownLatch(2)
  146. // log handler will be called on bg thread
  147. doAnswer {
  148. loggerThreadId = Thread.currentThread().id
  149. latch.countDown()
  150. it.callRealMethod()
  151. }.whenever(logHandler).loadLogFiles(any())
  152. // os handler will be called on bg thread
  153. whenever(osHandler.post(any())).thenAnswer {
  154. latch.countDown()
  155. true
  156. }
  157. // GIVEN
  158. // logger event loop is running
  159. logger.start()
  160. // WHEN
  161. // messages are logged
  162. // log contents are requested
  163. logger.d("tag", "message 1")
  164. logger.d("tag", "message 2")
  165. logger.d("tag", "message 3")
  166. logger.load(listener)
  167. val called = latch.await(LATCH_WAIT, TimeUnit.SECONDS)
  168. assertTrue("Response not posted", called)
  169. // THEN
  170. // log contents are loaded on background thread
  171. // logs are posted to main thread handler
  172. // contents contain logged messages
  173. // messages are in order of writes
  174. assertNotEquals(currentThreadId, loggerThreadId)
  175. val postedCaptor = ArgumentCaptor.forClass(Runnable::class.java)
  176. verify(osHandler).post(capture(postedCaptor))
  177. postedCaptor.value.run()
  178. val logsCaptor = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<LogEntry>>
  179. val sizeCaptor = ArgumentCaptor.forClass(Long::class.java)
  180. verify(listener).invoke(capture(logsCaptor), capture(sizeCaptor))
  181. assertEquals(MESSAGE_COUNT, logsCaptor.value.size)
  182. assertTrue("message 1" in logsCaptor.value[0].message)
  183. assertTrue("message 2" in logsCaptor.value[1].message)
  184. assertTrue("message 3" in logsCaptor.value[2].message)
  185. }
  186. @Test
  187. fun `log level can be decoded from tags`() {
  188. Level.values().forEach {
  189. val decodedLevel = Level.fromTag(it.tag)
  190. assertEquals(it, decodedLevel)
  191. }
  192. }
  193. @Test
  194. fun `queue limit is enforced`() {
  195. // GIVEN
  196. // logger event loop is no running
  197. // WHEN
  198. // queue is filled up to it's capacity
  199. for (i in 0 until QUEUE_CAPACITY + 1) {
  200. logger.d("tag", "Message $i")
  201. }
  202. // THEN
  203. // overflow flag is raised
  204. assertTrue(logger.lostEntries)
  205. }
  206. @Test
  207. fun `queue overflow warning is logged`() {
  208. // GIVEN
  209. // logger loop is overflown
  210. for (i in 0..QUEUE_CAPACITY + 1) {
  211. logger.d("tag", "Message $i")
  212. }
  213. // WHEN
  214. // logger event loop processes events
  215. //
  216. logger.start()
  217. // THEN
  218. // overflow occurrence is logged
  219. whenever(osHandler.post(any())).thenAnswer {
  220. (it.arguments[0] as Runnable).run()
  221. true
  222. }
  223. // load continuously until we find expected log line or we give up
  224. var lostMessageWarning: LogEntry? = null
  225. for (i in 0..10) {
  226. val loaded = CountDownLatch(1)
  227. logger.load { entries, totalLogSize ->
  228. lostMessageWarning = entries.find {
  229. it.message.contains("Logger queue overflow")
  230. }
  231. loaded.countDown()
  232. }
  233. val ok = loaded.await(1000, TimeUnit.MILLISECONDS)
  234. assertTrue("load callback not invoked; did you enable handler mock?", ok)
  235. if (lostMessageWarning != null) {
  236. break
  237. } else {
  238. Thread.sleep(1000)
  239. }
  240. }
  241. assertNotNull(lostMessageWarning)
  242. }
  243. @Test
  244. fun `all log files are deleted`() {
  245. val latch = CountDownLatch(1)
  246. doAnswer {
  247. it.callRealMethod()
  248. latch.countDown()
  249. }.whenever(logHandler).deleteAll()
  250. // GIVEN
  251. // logger is started
  252. logger.start()
  253. // WHEN
  254. // logger has some writes
  255. // logs are deleted
  256. logger.d("tag", "message")
  257. logger.d("tag", "message")
  258. logger.d("tag", "message")
  259. logger.deleteAll()
  260. // THEN
  261. // handler writes files
  262. // handler deletes all files
  263. assertTrue(latch.await(LATCH_WAIT, TimeUnit.SECONDS))
  264. verify(logHandler, times(MESSAGE_COUNT)).write(any())
  265. verify(logHandler).deleteAll()
  266. val loaded = logHandler.loadLogFiles(logHandler.maxLogFilesCount)
  267. assertEquals(EMPTY, loaded.lines.size)
  268. assertEquals(EMPTY_LONG, loaded.logSize)
  269. }
  270. @Test
  271. @Suppress("TooGenericExceptionCaught")
  272. fun `thread interruption is handled while posting log message`() {
  273. Thread {
  274. val callerThread = Thread.currentThread()
  275. // GIVEN
  276. // logger is running
  277. // caller thread is interrupted
  278. logger.start()
  279. callerThread.interrupt()
  280. // WHEN
  281. // message is logged on interrupted thread
  282. var loggerException: Throwable? = null
  283. try {
  284. logger.d("test", "test")
  285. } catch (ex: Throwable) {
  286. loggerException = ex
  287. }
  288. // THEN
  289. // message post is gracefully skipped
  290. // thread interruption flag is not cleared
  291. assertNull(loggerException)
  292. assertTrue("Expected current thread to stay interrupted", callerThread.isInterrupted)
  293. }.apply {
  294. start()
  295. join(TIMEOUT)
  296. }
  297. }
  298. }